header={ "chef": "BeatRig", "dependencies": "ffprobe,ffmpeg", "title": "Netflix - Streaming Loudness", "description": "Validate or fix a file to match the online streaming loudness standard used by Netflix Online Streaming", "instructions": "Drop file(s) here", "recipe_version": "1.69", "tags": "audio, loudness, ATSC A/85, online streaming", "type": "demo", "os": "windows,macOS", "palette": "Clean Slate", "spice": "BQ==:G+Hy36kJcnQQYYavb/JV2Il6eCYr6h4hRJHCF9rRbhX5BcazL6cuTgUMdz0xA50VWA5PmmV0Q7aWkiUtNFtbk991l+t0e8IBXMReMIT1GNjnZZDv7n2YRAo0xcSQbY0ixjG6LLH4wGKnspKAoA/v69iLn6N2IH0XzIZwTGdHSo4=", "flavour": "y7eAkU53ghnm99dTuQYk07+n4jZvxFASHQ4ekpvSEVqHu4J22ep8n9RLe9rRdUPC15cvB38v0JwL2W9sEhHiWVPY/wFqCs9HjuHsArEgzQxFg+SMbzhGfKCrgrOs87E1av0atNcbKDXsWlytGUE/OzS+SmaKyAEOcURDqxZemQo=", "time": 1737408852, "core_version": "0.7.3", "magnetron_version": "1.0.340", "functions": "main,onConfig,onAbout", "uuid": "042923a51b144b07b9dd9d96e92858bb" }; // ============================================================================= // DEFAULT SETTINGS var defaults = { "LUFS": -23, "tolerance": true, "tolerance_val": 0.5, "Dial": false, "TruePeak": true, "TruePeak_val": -1, "Max_S": false, "Max_S_val": 5, "Max_M": false, "Max_M_val": 8, "nametag_val": "-NETFLIX", "subFolder_val": "NETFLIX", }; // ############################################################################# // AudLoud 0.7.2R08 // ############################################################################# // ============================================================================= // GLOBALS var settings = {}; // SETTINGS var settingsFile = ""; // LOCATION OF THE SETTINGS FILE var fileTypes = []; // ALLOWED FILE TYPES var has_ffmpeg = "NOTDEFINED"; // CHECK IF FFMPEG & FFPROBE ARE AVAILABLE FOR TRUE PEAK LIMITING var has_sox = "NOTDEFINED"; // CHECK IF FFMPEG HAS SOX RESAMPLER BUILT IN, WILL BE TESTET THE FIRST TIME NEEDED var curDuration = 0.; // DURATION OF THE CURRENT FILE FOR PROGRESS var files = []; // FILES TO BE PROCESSED var needsfixing = 0; // COUNT FILES THAT NEED FIXING var needslimiting = 0; // COUNT FILES THAT NEED LIMITING // ============================================================================= // SET DEFAULT VALUES function init(){ // LOCATION OF THE SETTINGS FILE settingsFile = folders.content+gvar.pss+'default.txt'; // SETTINGS settings = { "LUFS" : defaults.LUFS || -23, // Target Loudness "tolerance" : defaults.tolerance || true, // Tolerance (Permitted Deviation) "tolerance_val" : defaults.tolerance_val || 0.5, // Tolerance Value "Dial" : defaults.Dial || false, // Use Dialog Intelligence "TruePeak" : defaults.TruePeak || true, // Max True Peak Level "TruePeak_val" : defaults.TruePeak_val || -1, // Max True Peak Level Value "tplimit" : defaults.tplimit || true, // Enable True Peak Limiter "Max_S" : defaults.Max_S || false, // Max Short Term "Max_S_val" : defaults.Max_S_val || 0, // Max Short Term Value "Max_M" : defaults.Max_M || false, // Max Momentary "Max_M_val" : defaults.Max_M_val || 0, // Max Momentary Value "LRA" : defaults.LRA || false, // Loudness Range "LRA_val" : defaults.LRA_val || 0, // Loudness Range Value "export" : defaults.export || "ask", // ask = DEFAULT, never, always "overwrite" : defaults.overwrite || false, // overwrite existing files "files" : defaults.files || "fixed", // fixed (Default), all "customFolder" : defaults.customFolder || false, // Move fixed files to a custom folder "customFolder_path" : defaults.customFolder_path || folders.home, // By default this is the users home folder "nametag" : defaults.nametag || true, // Tag added to fixed file "nametag_val" : defaults.nametag_val || "-R128", // Tag added to fixed file "subFolder" : defaults.subFolder || false, // Move fixed files to a subfolder "subFolder_val" : defaults.subFolder_val || "R128", // Move fixed files to a subfolder "validate" : defaults.validate || true // Re-analyse after processing, slow but more reliable }; // ALLOWED FILE TYPES fileTypes = [ 'wav', 'aif', 'aiff', ]; } // ============================================================================ function main() { // ----------------------------- // START THE MACHINE _e("TITLE", header.title); _e("VERSION:", header.recipe_version +" // "+ header.time ); setProgress(0); // ----------------------------- // DEFAULT SETTINGS init(); // ----------------------------- // CUSTOM SETTINGS loadSettings(); // ----------------------------- // GET ALL FILES FROM THE APP files = getFiles(); // ------------------------------------ // IF NO FILES, WARN AND STOP if( files.length == 0) abort("Add some files first!"); // ------------------------------------ files.forEach(item => { // NO ERROR var err = false; // START WITH EMPTY TOOLTIP ARRAY item.ttip = []; var path = getPathInfo(item.path); // CHECK IF FILE CAN BE READ if(!path.isfile){ err = "The file could not be read"; } // CHECK IF FILE IS RIGHT EXTENSION else if(fileTypes.indexOf( toLowerCase(path.ext) ) == -1){ err = "The file can not be checked."; item.ttip.push("Please add a WAV or AIFF file"); } item.file_icon = err ? "ExclamationCircle" : "square"; item.file_icon_color = err ? "FFFF0000" : "FFdddddd"; item.file_status = err ? err : ""; item.skip = err ? 1 : 0; // skip if error item.dialogmissing = false; // if dialog is required but not found _set(item); }); // ------------------------------------------------------------------------- // ANALYSE // ------------------------------------------------------------------------- files.forEach((item, index) => { // ------------------------------------ var err = false; // ------------------------------------ // CHECK IF USER CANCELLED if( isCanceled() ) abort("Processing Cancelled"); // ------------------------------------ // SKIP INVALID FILES if( item.skip == 1 ) return; // ------------------------------------ // SET UI MESSAGE & CURRENT FILE ICON setMainMessage("analysing "+(index+1)+"/"+files.length); item.file_icon = "CaretSquareORight"; item.file_icon_color = "FF777777"; item.file_status = "analysing..."; _set(item); // ------------------------------------ // FILE SPECS var path = getPathInfo(item.path); var infile = validateLoudness(item.path); // ------------------------------------ // MESSAGES item.ttip.push('SOURCE FILE: '+path.filename); item.ttip.push(...infile.msg); // ------------------------------------ // OUTPUT FOLDER item.outPath = ( settings.customFolder ? settings.customFolder_path : path.folder ) + ( settings.subFolder ? gvar.pss + settings.subFolder_val : ''); // ------------------------------------ // OUTPUT FILENAME item.outFile = item.outPath + gvar.pss + path.basename + (settings.nametag ? settings.nametag_val : "") + '.' + path.ext; // ------------------------------------ // FILE EXISTS BUT REMOVE IF OVERWRITE IS SET if(fileExists(item.outFile) && settings.overwrite){ deleteFile(item.outFile); } // ------------------------------------ // CHECK IF DIALOG IS REQUIRED BUT NOT FOUND if(settings.Dial && infile.levels.Dialog_percentage < 5){ item.dialogmissing = true; item.ttip.push('x No dialog detected!'); } // ------------------------------------ // START CHECKING THE FILE // CANNOT OVERWRITE SELF if (item.path === item.outFile){ err = "Cannot overwrite source file!"; } else if (files.some((file, x) => x !== index && file.outFile === files[index].outFile)) { err = "The output filename is not unique!"; } // FILE EXISTS else if (fileExists(item.outFile)) { err = "The output file exists!"; } // COULD NOT READ THE FILE else if (!infile) { err = "Could not analyse the file!"; } // VALID FILE - NO NEED TO FIX else if ( infile.status == 1 && item.dialogmissing == true ) { item.fixme = 0; // no need to fix item.file_icon = "CheckSquare"; item.file_icon_color = "FFFF7F50"; item.file_status = "Passed, but no dialog detected"; } // VALID FILE - NO NEED TO FIX else if ( infile.status == 1 ) { item.fixme = 0; // no need to fix item.file_icon = "CheckSquare"; item.file_icon_color = "FF008800"; item.file_status = "Passed"; } // NOT VALID FILE - NEED TO FIX else { // COUNT FILES THAT NEED FIXING needsfixing++; // CHECK IF LIMITING IS NEEDED let limiting =(infile.levels.TruePeak + infile.levels.AdjustLevel > (settings.TruePeak_val - 0.1) ? true : false); if(limiting) needslimiting++; // SET ICONS AND COLORS item.file_icon = "MinusSquareO"; item.file_icon_color = "FFff0000"; item.file_status = "Did not validate"; item.fixme = 1; // needs to be fixed item.LUFS = Math.round(infile.levels.LUFS * 100) / 100; item.TruePeak = Math.round(infile.levels.TruePeak * 100) / 100; item.process_adjust = infile.levels.AdjustLevel_NoLimit; item.process_limit = limiting; if(gvar.demo) item.ttip.push('\n\n\n--------\nBuy Magnetron.APP to fix files'); } // ------------------------------------ if (err) { item.fixme = -1; item.file_icon = "ExclamationCircle"; item.file_icon_color = "FFff0000"; item.file_status = err; } _set(item); // ------------------------------------ }); // ============================================================================= // IF SETTINGS IS AKS THEN ASK var doProcess = false; // ALWAYS EXPORT if( !gvar.demo && (settings.export == "yes" )){ doProcess = true; } // ASK TO EXPORT else if( !gvar.demo && settings.export == "ask" && needsfixing > 0){ if (askDialog().returns == 1) doProcess = true; } // ============================================================================= // PROCESS if( doProcess ) { // -------- // SET FILES TO PROCESSING files.forEach((item, index) => { if( item.fixme == 1 ){ item.file_icon = "square"; item.file_icon_color = "FFdddddd"; item.file_status = "..."; _set(item); } }); // -------- setMainMessage("Preparing..."); // -------- // CHECK IF FFMPEG & FFPROBE ARE NEEDED AND SET AND REALY WORK if(needslimiting > 0 && (has_ffmpeg === "NOTDEFINED" || has_sox === "NOTDEFINED")) { // CHECK IF FFMPEG & FFPROBE ARE SET has_ffmpeg = !checkDependencies("ffprobe,ffmpeg"); // CHECK FFPROBE VERSION TO SEE IF IT IS WORKING var ffprobeVersion = cmd("ffprobe", ["-version"]); if(!ffprobeVersion) { abort('FFMPEG FAILED\nPlease check if the command app is available'); } // CHECK IF FFMPEG VERSION TO SEE IF IT IS WORKING var ffmpegVersion = cmd("ffmpeg", ["-version"]); if(!ffmpegVersion) { abort('FFMPEG FAILED\nPlease check if the command app is available'); } // CHECK IF FFMPEG HAS SOX RESAMPLER BUILT IN has_sox = ffmpegVersion.includes("--enable-libsoxr"); } // -------- // LOOP FILES THAT NEEDS FIXING files.forEach((item, index) => { setMainMessage("processing "+(index+1)+"/"+files.length); // SKIP FILES THAT CAN'T BE FIXED if( item.fixme == -1 ) return; // CHECK IF OUTPUT FOLDER EXISTS OR CREATE if( !fileExists(item.outPath) ) { makeFolder(item.outPath); } // IF THE FILE IS ALLREADY VALID, // JUST MOVE IT if(item.fixme == 0 && settings.files == 'all') { copyFile(f.path, f.outFile); item.ttip.push('- Copied without changes'); } // IF IT NEEDS FIXING else if(item.fixme == 1) { item.file_icon = "CaretSquareORight"; item.file_icon_color = "FF555555"; item.file_status = "processing..."; _set(item); item.ttip.push('\n\nADJUST'); item.ttip.push('- level changed '+toFixed(item.process_adjust, 1)+'dB'); // COUNT ERRORS var err = 0; var t = processFile(item); item.ttip.push(...t.msg); // IF PASSED, ANALYZE OUTPUT FILE, REDUNDANT AND SLOW BUT EXTRA SAFE if(settings.validate) { setMainMessage("validating "+(index+1)+"/"+files.length); var outfileLevels = validateLoudness(item.outFile); item.ttip.push('\n\nOUTPUT:'); item.ttip.push(...outfileLevels.msg); if(outfileLevels.status != 1) err++; } // ERROR CHHECKING if ( err > 0 ) { item.file_icon = "ExclamationCircle"; item.file_icon_color = "FFff0000"; item.file_status = "Could not be fixed"; _set(item); // REMOVE FILE IF ERRORS STILL EXIST if(fileExists(item.outFile)) deleteFile(item.outFile); } // FIXED else { item.file_icon = "CheckSquare"; // if dialog was missing if(item.dialogmissing){ item.file_icon_color = "FFFF7F50"; item.file_status = "Fixed, but no dialog detected!"; } // if process_limit else if(item.process_limit){ item.file_icon_color = "FFFF7F50"; item.file_status = "Fixed, but with limiting!"; } else{ item.file_icon_color = "FF0000FF"; item.file_status = "Fixed!"; } } } // TOOLTIP _set(item); // end loop }); // end process } // ALL DONE setProgress(0); setMainMessage(""); // end main } // ============================================================================= // GET THE LOUDNESS SPECIFICATIONS AND CHECK THEM, // IF NOT VALID RETURN AN ARRAY WITH ERRORS function validateLoudness(file) { // RETURN VALUE var rtn = []; var err = 0; // ANALYSES AUDIO LEVELS var levels = levelFileAnalyse( file , { "AdjustTargetType": "LUFS", "AdjustTargetLevel": settings.LUFS, "DialogGate": settings.Dial, }); // NO RESULTS if (levels == null) return false; // NO LOUDNESS FOUND if ( levels.LUFS == undefined ) return false; // CHECK LUFS const tolerance = settings.tolerance ? settings.tolerance_val : 0.1; if (Math.abs(settings.LUFS - levels.LUFS) >= tolerance) { rtn.push(`x Loudness did not validate (${toFixed(levels.LUFS, 1)}LUFS)`) && err++; } else { rtn.push(`✓ Loudness (${toFixed(levels.LUFS, 1)}LUFS)`); } // CHECK TRUE PEAK if (settings.TruePeak) { const [peak, peakVal] = [toFixed(levels.TruePeak, 1), toFixed(settings.TruePeak_val, 1)]; if (levels.TruePeak > settings.TruePeak_val) { rtn.push(`x Peak Level exceeded ${peakVal}dBTP (${peak}dBTP)`) && err++; } else { rtn.push(`✓ Peak Level (${peak}dBTP)`); } } // CHECK MAX S if (settings.Max_S) { const [maxS, maxSVal] = [toFixed(levels.Max_S, 1), toFixed(settings.Max_S_val, 1)]; if (levels.Max_S > settings.Max_S_val) { rtn.push(`x MaxS exceeded ${maxSVal} (${maxS}LU)`) && err++; } else { rtn.push(`✓ MaxS (${maxS}LU)`); } } // CHECK MAX M if (settings.Max_M) { const [maxM, maxMVal] = [toFixed(levels.Max_M, 1), toFixed(settings.Max_M_val, 1)]; if (levels.Max_M > settings.Max_M_val) { rtn.push(`x MaxM exceeded ${maxMVal} (${maxM}LU)`) && err++; } else { rtn.push(`✓ MaxM (${maxM}LU)`); } } // CHECK LRA if (settings.LRA) { const [lra, lraVal] = [toFixed(levels.LRA, 1), toFixed(settings.LRA_val, 1)]; if (levels.LRA > settings.LRA_val) { rtn.push(`x LRA exceeded ${lraVal} (${lra}LU)`) && err++; } else { rtn.push(`✓ LRA (${lra}LU)`); } } // RETURN return { "status" : ( err > 0 ? 0 : 1 ), // if error = 0, passed = 1 "msg" : rtn, "levels" : levels }; } // ============================================================================= // LEVEL ANALYSE PROGRESS CALLBACK function LevelFileAnalyse_Progress(file, progress) { setProgress( progress * 100 ); } // ============================================================================= // LEVEL PROCESS PROGRESS CALLBACK function LevelFileProcess_Progress(file, progress) { setProgress( progress * 100 ); } // ============================================================================= // FFMPEG CALLBACK function cmd_callback(cmd_name, cmd_output) { // DUE TO A BUG IN THE FFMPEG CALLBACK THE MS IS SHOWN AS MICRO SECONDS, // NOT MILISECCONDS. SO WE NEED TO DIVIDE BY 1000000 TO GET THE CORRECT TIME if (cmd_name === "ffmpeg" && cmd_output.length > 0 && curDuration > 0) { const progress = Math.round((parseInt(cmd_output.match(/out_time_us=(\d+)/)?.pop() || 0, 10) / 1000000) / curDuration * 100); setProgress(progress); } return { "terminate": isCanceled(), // use this option to end the process if there is no better alternative "input": "" // send console input, like a quit signal (followed by a 'return' 0x0D) }; } // ============================================================================= // ON CONFIG, SHOW DIALOG AND SAVE SETTINGS function onConfig() { if(gvar.demo){ } else{ // ----------------------------- // DEFAULT SETTINGS init(); // ----------------------------- // LOAD CUSTOM SETTINGS loadSettings(); // ----------------------------- // IF THE CUSTOM PATH IS INVALID, SET IT TO HOME FOLDER if(!fileExists(settings.customFolder_path)){ settings.customFolder_path = folders.home; } // ----------------------------- // SETTINGS DIALOG var r = showSettings(); // ----------------------------- // IF RESET IS PUSHED if(r.returns === -1) { deleteFile(settingsFile); // DELETE THE SETTINGS FILE init(); // RESET TO DEFAULT SETTINGS onConfig(); // OPEN THIS CONFIG FUNCTION AGAIN } // ----------------------------- // IF SAVED WAS PUSHED else if(r.returns === 1) { // CHECK THAT SUBFOLDER DOES NOT CONTAIN SLASHES settings.subFolder_val = settings.subFolder_val.replace(/\//g, ''); writeFile( settingsFile, JSON.stringify(settings, null, 4) ); } // ----------------------------- } } // ============================================================================= // ON ABOUT, SHOW POPUP WITH BUTTON TO OPEN WEBSITE function onAbout() { var args = { "info": { "type": "texteditor", "bounds" : { "y" : 70, "w" : 400, "h" : 150 }, "visible": true, "multiline" : true, "wordwrap" : true, "default" : header.description+'\n\nMore information on the recipes page on our website.', "enabled" : false }, "showpage" : { "type" : "button", "label" : "show page", "returns" : 1 }, "close" : { "type" : "button", "label" : "close", "returns" : 0 } }; var r = dialog(header.title, "" , "", args); if (r.showpage == 1) launchInBrowser('https://magnetron.app/recipes/uuid/'+header.uuid+'/'); } // ============================================================================= // LOAD DEFAULT VALUES function loadSettings() { if (fileExists(settingsFile)) { Object.assign(settings, JSON.parse(readFile(settingsFile))); } return true; } // ============================================================================= function showSettings() { var form = { // -------------------------------- "spacer0": { "type": "text", "label": "","just": "c", "bounds": { "x": 20, "w": 550, "h": 20 }, }, // -------------------------------- "heading1" : { "type" : "text", "label" : "// TARGET - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -", "just" : "l", "bounds" : { "x": 0, "w" : 600, "h" : 40 }, }, // -------------------------------- "LUFS_label" : { "type" : "text", "label" : "Loudnes", "bounds" : { "x": 0, "w" : 160, }, }, "LUFS_label2" : { "type" : "text", "label" : "0LU = ", "enabled" : false, "bounds" : { "y" : -1, "x": 0, "x" : 110, "w" : 80, }, }, "LUFS": { "type": "slider", "style": "slider", "visible": true, "range" : { "min": -99, "max" : 0, "interval" : .1, "decimals" : 1 }, "bounds" : { "y" : -1, "x": 180, "w" : 100, }, "value": settings.LUFS }, "LUFS_d" : { "type" : "text", "label" : "LUFS", "bounds" : { "y" : -1,"x": 290, "w" : 160, }, }, // -------------------------------- "tolerance_label" : { "type" : "text", "label" : "Tolerance", "bounds" : { "x": 0, "w" : 160, }, }, "tolerance": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.tolerance }, "tolerance_val": { "type": "slider", "style": "slider", "visible": true, "range" : { "min": 0, "max" : 10, "interval" : .1, "decimals" : 1 }, "bounds" : { "y" : -1, "x": 180, "w" : 100, }, "value": settings.tolerance_val }, "tolerance_d" : { "type" : "text", "label" : "+/- LU", "bounds" : { "y" : -1,"x": 290, "w" : 160, }, }, // -------------------------------- "dial_label" : { "type" : "text", "label" : "Dialogue Gate", "bounds" : { "x": 0, "w" : 160, }, }, "dial": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.dial }, // -------------------------------- "spacer0a": { "type": "text", "label": "","just": "c", "bounds": { "x": 20, "w": 550, "h": 20 }, }, // -------------------------------- "heading3" : { "type" : "text", "label" : "// VALIDATE - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -", "just" : "l", "bounds" : { "x": 0, "w" : 600, "h" : 40 }, }, // -------------------------------- "TruePeak_label" : { "type" : "text", "label" : "TruePeak", "bounds" : { "x": 0, "w" : 160, }, }, "TruePeak": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.TruePeak }, "TruePeak_val": { "type": "slider", "style": "slider", "visible": true, "range" : { "min": -9, "max" : 0, "interval" : .1, "decimals" : 1 }, "bounds" : { "y" : -1, "x": 180, "w" : 100, }, "value": settings.TruePeak_val }, "TruePeak_d" : { "type" : "text", "label" : "dBFS", "bounds" : { "y" : -1,"x": 290, "w" : 160, }, }, // -------------------------------- // TP Limiter "tplimit_label" : { "type" : "text", "label" : "Limit", "just" : "l", "bounds" : { "y": -1, "x": 450, "w" : 100, }, }, "tplimit": { "type": "togglebox", "bounds" : { "y": -1, "x": 560, "w" : 25, }, "label" : "", "toggle" : settings.tplimit }, // -------------------------------- "Max_S_label" : { "type" : "text", "label" : "Max-S", "bounds" : { "x": 0, "w" : 160, }, }, "Max_S": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.Max_S }, "Max_S_val": { "type": "slider", "style": "slider", "visible": true, "range" : { "min": 0, "max" : 12, "interval" : .1, "decimals" : 1 }, "bounds" : { "y" : -1, "x": 180, "w" : 100, }, "value": settings.Max_S_val }, "Max_S_d" : { "type" : "text", "label" : "LU", "bounds" : { "y" : -1,"x": 290, "w" : 160, }, }, // -------------------------------- "Max_M_label" : { "type" : "text", "label" : "Max-M", "bounds" : { "x": 0, "w" : 160, }, }, "Max_M": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.Max_M }, "Max_M_val": { "type": "slider", "style": "slider", "visible": true, "range" : { "min": 0, "max" : 20, "interval" : .1, "decimals" : 1 }, "bounds" : { "y" : -1, "x": 180, "w" : 100, }, "value": settings.Max_M_val }, "Max_M_d" : { "type" : "text", "label" : "LU", "bounds" : { "y" : -1,"x": 290, "w" : 160, }, }, // -------------------------------- "LRA_label" : { "type" : "text", "label" : "LRA", "bounds" : { "x": 0, "w" : 160, }, }, "LRA": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.LRA }, "LRA_val": { "type": "slider", "style": "slider", "visible": true, "range" : { "min": 0, "max" : 20, "interval" : .1, "decimals" : 1 }, "bounds" : { "y" : -1, "x": 180, "w" : 100, }, "value": settings.LRA_val }, "LRA_d" : { "type" : "text", "label" : "LU", "bounds" : { "y" : -1,"x": 290, "w" : 160, }, }, // -------------------------------- "spacer1": { "type": "text", "label": "","just": "c", "bounds": { "x": 20, "w": 550, "h": 20 }, }, // -------------------------------- "heading2" : { "type" : "text", "label" : "// EXPORT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -", "just" : "l", "bounds" : { "x": 0, "w" : 600, "h" : 40 }, }, // -------------------------------- // Fix "export_label" : { "type" : "text", "label" : "Export", "just" : "l", "bounds" : { "x": 0, "w" : 100, }, }, "export" : { "type" : "combobox", "default" : settings.export, "items" : "ask,yes,no", "bounds" : { "y" : -1, "x": 180, "w" : 120, }, "returns" : 0, }, // -------------------------------- // Overwrite "overwrite_label" : { "type" : "text", "label" : "Overwrite", "just" : "l", "bounds" : { "y": -1, "x": 450, "w" : 100, }, }, "overwrite": { "type": "togglebox", "bounds" : { "y": -1, "x": 560, "w" : 25, }, "label" : "", "toggle" : settings.overwrite }, // -------------------------------- "files_label" : { "type" : "text", "label" : "Files", "just" : "l", "bounds" : { "x": 0, "w" : 100, }, }, "files" : { "type" : "combobox", "default" : settings.files, "items" : "all,fixed", "bounds" : { "y" : -1, "x": 180, "w" : 120, }, "returns" : 0, }, // -------------------------------- "customFolder_label" : { "type" : "text", "label" : "Custom folder", "just" : "l", "bounds" : { "x": 0, "w" : 160, }, }, "customFolder": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.customFolder }, "customFolder_path" : { "type" : "fileselect", "path" : settings.customFolder_path, "editable" : false, "dir" : true, "saving" : true, "label" : "empty", "bounds" : { "y": -1, "x": 180, "w" : 400, }, "visible": false, }, // -------------------------------- "subFolder_label" : { "type" : "text", "label" : "Sub folder", "just" : "l", "bounds" : { "x": 0, "w" : 160, }, }, "subFolder": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.subFolder }, "subFolder_val": { "type": "texteditor", "bounds" : { "y": -1, "x" : 180, "w" : 400, }, "multiline" : false, "wordwrap" : false, "default" : settings.subFolder_val, "visible": true, }, // -------------------------------- "nametag_val_label" : { "type" : "text", "label" : "Name tag", "just" : "l", "bounds" : { "x": 0, "w" : 160, }, }, "nametag": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.nametag }, "nametag_val": { "type": "texteditor", "bounds" : { "y": -1, "x" : 180, "w" : 400, }, "multiline" : false, "wordwrap" : false, "default" : settings.nametag_val, "visible": false, }, // -------------------------------- "validate_label" : { "type" : "text", "label" : "Revalidate", "bounds" : { "x": 0, "w" : 160, }, }, "validate": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.validate }, // -------------------------------- "spacer3" : { "type" : "text", "label" : " ", "just" : "r", "bounds" : { "x": 0, "w" : 200, "h" : 20 }, }, // -------------------------------- "reset" : { "type" : "button", "label" : "reset", "bounds" : { "x": 330, "w" : 80 }, "returns" : -1 }, "cancel" : { "type" : "button", "label" : "cancel", "bounds" : { "y": -1, "x": 415, "w" : 80 }, "returns" : 0 }, "save" : { "type" : "button", "label" : "save", "bounds" : { "y": -1, "x": 500, "w" : 80 }, "returns" : 1 }, // -------------------------------- }; // -------------------------------- return dialog(header.title, "", "w", form); } // ============================================================================ // DIALOG CALLBACK function dialog_callback(props) { const directMapping = { export: "text", files: "text", overwrite: "toggle", customFolder: "toggle", customFolder_path: "path", nametag: "toggle", nametag_val: "text", subFolder: "toggle", subFolder_val: "text", LUFS: "value", TruePeak: "toggle", TruePeak_val: "value", tplimit: "toggle", tolerance: "toggle", tolerance_val: "value", Max_S: "toggle", Max_S_val: "value", Max_M: "toggle", Max_M_val: "value", LRA: "toggle", LRA_val: "value", dial: "toggle", validate: "toggle" }; // Dynamische toekenning if (directMapping[props.name]) { settings[props.name] = props[directMapping[props.name]]; } // ---------------- // SHOW/HIDE WHEN EXPORT var exp = ( settings['export'] == 'yes' || settings['export'] == 'ask' ? true : false ); // ---------------- // UPDATE DIALOG dialog({ "TruePeak_val" : { "visible" : settings['TruePeak'] }, "TruePeak_d" : { "visible" : settings['TruePeak'] }, "tplimit_label" : { "visible" : settings['TruePeak'] }, "tplimit" : { "visible" : settings['TruePeak'] }, "tolerance_val" : { "visible" : settings['tolerance'] }, "tolerance_d" : { "visible" : settings['tolerance'] }, "Max_S_val" : { "visible" : settings['Max_S'] }, "Max_S_d" : { "visible" : settings['Max_S'] }, "LRA_val" : { "visible" : settings['LRA'] }, "LRA_d" : { "visible" : settings['LRA'] }, "Max_M_val" : { "visible" : settings['Max_M'] }, "Max_M_d" : { "visible" : settings['Max_M'] }, "files_label" : { "enabled" : exp }, "files" : { "enabled" : exp, "visible" : exp, "default" : settings['files'] }, "overwrite_label" : { "enabled" : exp, "visible" : exp }, "overwrite" : { "enabled" : exp, "visible" : exp }, "validate_label" : { "enabled" : exp }, "validate" : { "enabled" : exp, "visible" : exp }, "customFolder_label" : { "enabled" : exp, }, "customFolder" : { "enabled" : exp, "visible" : exp }, "customFolder_path" : { "enabled" : exp, "visible" : exp && settings['customFolder'] }, "subFolder_label" : { "enabled" : exp, }, "subFolder" : { "enabled" : exp, "visible" : exp }, "subFolder_val" : { "enabled" : exp, "visible" : exp && settings['subFolder'] }, "nametag_val_label" : { "enabled" : exp, }, "nametag" : { "enabled" : exp, "visible" : exp }, "nametag_val" : { "enabled" : exp, "visible" : exp && settings['nametag'] }, }); // ---------------- } // ============================================================================= // DIALOG THE ASK THE USER TO FIX OR SKIP function askDialog() { var form = { // -------------------------------- "spacer0": { "type": "text", "label": "","just": "c", "bounds": { "x": 20, "w": 550, "h": 20 }, }, // -------------------------------- "text" : { "type" : "text", "label" : "Some files did not validate.\nDo you want to try to fix them?", "bounds" : { "x": 0, "w" : 550, "h" : 100 }, }, // -------------------------------- "text2" : { "type" : "text", "label" : "", "bounds" : { "x": 0, "w" : 550, "h" : 150 }, }, "skip" : { "type" : "button", "label" : "skip", "bounds" : { "x": 340, "w" : 100, }, "returns" : 0 }, "fix" : { "type" : "button", "label" : "fix", "bounds" : { "y": -1, "x": 450, "w" : 100 }, "returns" : 1 }, // -------------------------------- "text3" : { "type" : "text", "label" : "", "bounds" : { "x": 0, "w" : 550, "h" : 250 }, }, }; return dialog(header.title, '', "w", form); } // ============================================================================= function processFile(f) { // ---------------------------------------- var msg = []; var err = 0; // ---------------------------------------- // PROCESS USING FFMPEG if(f.process_limit && has_ffmpeg) { // WE NEED THE SAMPLE RATE BECAUSE IT WILL UPSAMPLE THE FILE TO 192 FOR THE TRUE PEAK // FILTER AND WE NEED TO BRING IT BACK TO THE ORIGINAL SAMPLE RATE var ffprobe = cmd("ffprobe", [ '-v', 'error', '-select_streams', 'a:0', '-show_entries', 'format=size:stream=sample_rate,bits_per_sample,channels:format=duration', '-of', 'default=noprint_wrappers=1', '-i', f.path ]); const sample_rate = parseInt(ffprobe.match(/(?:sample_rate=)(.*?)(?=[\n\r])/m)?.[1] || -1); const channels = parseInt(ffprobe.match(/(?:channels=)(.*?)(?=[\n\r])/m)?.[1] || -1); // SET THE DURATION FOR THE PROGRESS BAR curDuration = parseFloat(ffprobe.match(/(?:duration=)(.*?)(?=[\n\r])/i)?.[1] || 1); // LOUDNORM FILTER // limiter setting is 0.2 below the target level for a little extra headroom // lowest level for the limiter is -9.0 dBTP const tpmax = Math.max( Math.round((settings.TruePeak_val - 0.2) * 1000) / 1000, -9.0 ) const tpmeasured = Math.round((f.TruePeak - 0.01) * 100) / 100; const tplimit = (settings.tplimit ? `:TP=` + tpmax + `:measured_TP=` + tpmeasured : ''); const loudnormFilter = `loudnorm=linear=true` + `:I=${Math.round(settings.LUFS * 100) / 100}` + `:measured_I=${Math.round(f.LUFS * 100) / 100}` + tplimit + `:print_format=summary`; // RESAMPLING const resampling = has_sox ? `aresample=resampler=soxr:osr=${sample_rate}:precision=28` : ['-ar', sample_rate]; // ADD MESSAGE msg.push('- processed using ffmpeg'); if (has_sox) { msg.push('- with sox resampling enabled'); } if (settings.tplimit) { msg.push('- with True Peak limiting enabled'); } // FFMPEG ARGUMENTS const ffmpegArgs = [ '-i', f.path, '-filter:a', `${loudnormFilter}${has_sox ? ',' + resampling : ''}`, ...(Array.isArray(resampling) && !has_sox ? resampling : []), '-ac', channels, '-loglevel', 'error', '-progress', 'pipe:1', '-nostats', f.outFile ]; // RUN THE FFMPEG COMMAND cmd("ffmpeg", ffmpegArgs); } // ---------------------------------------- // PROCESS USING BUILT IN PROCESSOR else { msg.push('- processed using built-in processor'); // adjust properties var args = { "AdjustLevel_NoLimit" : f.process_adjust, }; // PROCESS var outfile = levelFileProcess( f.path, f.outFile, args ); // CHECK IF THE FILE WAS PROCESSED if (!outfile || typeof outfile.error !== 'undefined') { err++; msg.push(outfile.error); } } // ---------------------------------------- return { "msg" : msg, "err" : err }; } // ============================================================================= // DEBUG HELPER function _e(...args) { var output = []; args.forEach((arg) => { if (typeof arg === "object") { output.push(JSON.stringify(arg, null, 2)); } else { output.push(arg); } }); echo(output.join("\n")); } // ============================================================================= // SET HELPER function _set(obj) { if(!obj) return; if(!obj.path) return; let path = obj.path; let index = obj.index || 0; // if file_icons exists, set the icon if(obj.file_icon) setFileIcon(path, index, obj.file_icon); // if file_icon_color exists, set the icon color if(obj.file_icon_color) setFileIconColor(path, index, obj.file_icon_color); // if file_status exists, set the status if(obj.file_status) setFileStatus(path, index, obj.file_status); // if file_tooltip exists, set the tooltip // if(obj.file_tooltip) setFileTooltip(path, index, obj.file_tooltip); // if ttip exists, implode the list and set the tooltip if(obj.ttip) setFileTooltip(path, index, obj.ttip.join("\n")); }