header={ "chef": "BeatRig", "recipe_version": "1.408", "title": "EAR.nu", "description": "Validate and/or fix radio ads before uploading them to ear.nu", "spice": "BQ==:G+Hy36kJcnQQYYavb/JV2Il6eCYr6h4hRJHCF9rRbhX5BcazL6cuTgUMdz0xA50VWA5PmmV0Q7aWkiUtNFtbk991l+t0e8IBXMReMIT1GNjnZZDv7n2YRAo0xcSQbY0ixjG6LLH4wGKnspKAoA/v69iLn6N2IH0XzIZwTGdHSo4=", "time": 1677588930, "magnetron_version": "1.0.222", "tags": "audio, radio", "core_version": "0.5.2", "palette": "Clean Slate", "dependencies": "ffmpeg,ffprobe", "type": "demo", "os": "windows,macOS", "functions": "main,onAbout", "instructions": "Drop file(s) here", "uuid": "ce28e2bebdc34c01a3c9e823e63da826", "flavour": "CBT+VcYmzWtOUupe+oinEvA/Nymj/UHNExOn2AleJJ2xStEwK7gXatW9WS0I4HlO8UIDRlGb9pf/cb1391EZMgzLoqxfy/F3g9Rd8QbO3uUvVX81CYCsDF88L+juUiH3JkrlQDUzBLTNzJ0jRNaFyolL27ETGeSlbnWZIMl70Gk=" }; // ============================================================================= // ON ABOUT, SHOW POPUP WITH BUTTON TO OPEN WEBSITE // ============================================================================ function onAbout() { var args = { "showpage" : { "type" : "button", "label" : "show page", "returns" : 1 }, "close" : { "type" : "button", "label" : "close", "returns" : 0 } }; var r = dialog(header.title, header.description, "w", args); if (r.showpage == 1) launchInBrowser('https://magnetron.app/recipes/ear-nu/'); } // ============================================================================= function main() { // ----------------------------- var settings = { "tag" : '-EAR', "target" : -9., "targetType" : 'peak', "ext" : [ 'wav', 'aif', 'aiff' ] }; // ----------------------------- // CHECK IF FFMPEG AND FFPROBE ARE AVAILABLE if(getAllowedApps("ffmpeg") == '' ) abort("FFMPEG is not available"); if(getAllowedApps("ffprobe") == '' ) abort("FFMPEG is not available"); // ----------------------------- setProgress(101); setMainMessage("recipe started"); // ----------------------------- // GET ALL FILES FROM THE APP var files = getFiles(); // ----------------------------- // IF NO FILES, WARN AND STOP if( files.length == 0) abort("Add some files first!"); // ----------------------------- // CLEAN UP THE FILE LIST for (i=0;i 0){ files[i]['file_icon'] = "exclamation"; files[i]['file_icon_color'] = "FFFF8800"; files[i]["file_tooltip"] = ( filespecs.details.length > 0 ? "Warnings:\n- "+filespecs.details.join("\n- ") : ""); files[i]['file_status'] = "Can't be fixed"; } // HAS WARNINGS BUT CAN FIXED else if(filespecs.warning > 0){ files[i]['file_icon'] = "exclamation"; files[i]['file_icon_color'] = "FFFF8800"; files[i]["file_tooltip"] = ( filespecs.details.length > 0 ? "Warnings:\n- "+filespecs.details.join("\n- ") : ""); files[i]["process_adjust"] = filespecs.levels.AdjustLevel_NoLimit; files[i]['file_status'] = "Need's fixing"; // MAKE LIST OF FILES THAT NEED FIXING needsfixing.push(i); } // HAS NO ERRORS AND WARNINGS else{ files[i]['file_icon'] = "check"; files[i]['file_icon_color'] = "FF009900"; files[i]["file_tooltip"] = ( filespecs.details.length > 0 ? "Warnings:\n- "+filespecs.details.join("\n- ") : ""); files[i]['file_status'] = "Valid"; } // UPDATE LIST setFiles(files); } } // ----------------------------- // PROCESSING if(needsfixing.length > 0 && !gvar.demo) { // get the location of first file to set the custom path var exportfolder = getPathInfo(files[needsfixing[0]].path); var r = askForExport(exportfolder.folder, settings.tag); if(r.fix == 1) { // loop files var files = getFiles(); for (i=0;i 0 || filespecs.warning > 0){ files[fileid]['file_icon'] = "exclamation"; files[fileid]['file_icon_color'] = "FFFF8800"; files[fileid]["file_tooltip"] = ( filespecs.details.length > 0 ? "Warnings:\n- "+filespecs.details.join("\n- ") : ""); files[fileid]['file_status'] = "Can't be fixed"; if( fileExists(exportpath) ) deleteFile(exportpath); } // FILE WAS FIXED else{ files[fileid]['file_icon'] = "check"; files[fileid]['file_icon_color'] = "FF000099"; files[fileid]["file_tooltip"] = ( filespecs.details.length > 0 ? "Warnings:\n- "+filespecs.details.join("\n- ") : ""); files[fileid]['file_status'] = "The file was fixed"; } // setFiles(files); } } } // ----------------------------- setProgress(100); setMainMessage("Done"); // ----------------------------- } // ============================================================================= // DETECT SILENCE. ABSOLUTE SILENCE CAN BE INTERPETED AS SIGNAL LOSS AND // CAUSE WARNINGS DURING BROADCASTING, WE'LL TRY TO CATCH DIGITAL SILENCE // WITH A THRESHOLD OF 1dB ABOVE MINIMUM.A 24BIT FILE HAS A 144dB RANGE // AND WE'LL USE A THRESHOLD OF 143. A 16BIT FILE HAS A 96dB RANGE SO // WE'LL USE A 95dB THRESHOLD function detect_silence(file, bitdepth) { var silence_threshold = (parseInt(bitdepth) == 16 ? 95 : 143); var silence_duration= 3; // 3SEC var silence = cmd("ffmpeg", [ '-i',file, '-af','silencedetect=n=-'+silence_threshold+'dB:d='+silence_duration, '-hide_banner', '-f','null', '-', ]); return (silence.indexOf('silencedetect') != -1); } // ============================================================================= // FFPROBE SPECS PARSER function fileSpecs(file) { // RETURN VALUES var rtn = { "bits_per_sample" : 0, "sample_rate" : 0, "duration" : 0, "channels" : 0, }; // FFPROBE COMMAND var ffprobe = cmd("ffprobe", [ '-v','error', '-select_streams','a:0', '-show_entries','format=size :stream=codec_type,codec_name,bit_rate,channels,sample_rate,bits_per_sample : format=duration,nb_streams', '-of','default=noprint_wrappers=1', '-i',file ]); // RETURN VALUES rtn.codec_name = searchRegEx(ffprobe, "(?:codec_name=)(.*?)(?=[\n\r])", "o", 1)[0]; rtn.channels = parseInt (searchRegEx(ffprobe, "(?:channels=)(.*?)(?=[\n\r])", "o", 1)[0]); rtn.channel_layout = searchRegEx(ffprobe, "(?:channel_layout=)(.*?)(?=[\n\r])", "o", 1)[0]; rtn.sample_rate = parseInt (searchRegEx(ffprobe, "(?:sample_rate=)(.*?)(?=[\n\r])", "o", 1)[0]); rtn.duration = parseFloat (searchRegEx(ffprobe, "(?:duration=)(.*?)(?=[\n\r])", "o", 1)[0] ); rtn.bits_per_sample = parseInt (searchRegEx(ffprobe, "(?:bits_per_sample=)(.*?)(?=[\n\r])", "o", 1)[0]); rtn.bitrate = parseInt(searchRegEx(ffprobe, "(?:bit_rate=)(.*?)(?=[\n\r])", "o", 1)[0]); rtn.filesize = parseInt(searchRegEx(ffprobe, "(?:size=)(.*?)(?=[\n\r])", "o", 1)[0]); return rtn; } // ============================================================================= // VALIDATE EAR SPECS function validateFile(f, specs, levels, silence) { // ---------------------------------------------------------------- // FILE PROPERTIES var error = 0; var warning = 0; var details = []; var count_human = (i+1)+"/"+files.length; // AUDIO CODEC if( ["pcm_s24le", "pcm_s16le", "pcm_s24be", "pcm_s16be"].indexOf(specs.codec_name) == -1 ){ warning++; details.push("Invalid audio codec. ("+specs.codec_name+")"); } // 48kHz SAMPLE RATE if( specs.sample_rate != 48000 ){ warning++; details.push("Invalid sample rate. ("+specs.sample_rate+")"); } // STEREO if( specs.channels != 2 ){ warning++; details.push("Not a stereo file"); } // 16 OR 24 BITS PER SAMPLE if( specs.bits_per_sample != 16 && specs.bits_per_sample != 24 ){ warning++; details.push("Invalid bit rate. ("+specs.bits_per_sample+")"); } // DURATION %5 = 0 if( specs.duration%5 != 0 ){ error++; details.push("Length is not a multiple of 5 seconds,"); } // SAMPLE PEAK OVER -9dBFS, WIDTH A 1 DECIMAL PRECICION if( Math.round(levels.Peak * 10) / 10 > settings.target ){ warning++; details.push("Peak level exceeds -9dBfs. ("+toFixed(levels.Peak, 2)+"dBfs)"); } // SAMPLE PEAK BELOW -10dBFS, WIDTH A 1 DECIMAL PRECICION if( Math.round(levels.Peak * 10) / 10 < -10. ){ warning++; details.push("The levels peak at "+toFixed(levels.Peak, 1)+"dBFS This technically not wrong but your commercial will be less loud."); } // SILENCE if( silence ){ error++; details.push("A silence longer than 3sec is detected,"); } // ---------------------------------------------------------------- // SET STATUS return { 'error' : error, 'warning' : warning, 'details' : details, 'levels' : levels, }; } // ============================================================================= // CONVERT FILE function exportFile(file, levels) { // new file var outFolder = file.folder+'/'+settings.exportFolder; var outFile = outFolder+'/'+file.basename+'.wav'; if(!fileExists(outFolder)) makeFolder(outFolder); var adjust = (Math.round(((settings.target-0.1)-levels.Peak)* 100.))/100.; // FFPROBE COMMAND var ffprobe = cmd("ffmpeg", [ '-i',file.path, '-acodec','pcm_s24le', '-ar','48000', '-filter:a','volume='+adjust+'dB', outFile, '-y', ]); // return ( fileExists(outFile) ? outFile : false ); } // ============================================================================= // CALLBACK LOUDNESS ANALYSING // this function gets called when busy LevelFileAnalyse. progress 0. to 1. function LevelFileAnalyse_Progress(file, progress){ setProgress(100 * progress); } function LevelFileProcess_Progress(file, progress){ setProgress(100 * progress); } // ============================================================================= // FFMPEG/FFPROBE CALLBACK function cmd_callback(cmd_name, cmd_output) { // if (cmd_output.length > 0) echo(cmd_name + " " + cmd_output); setProgress(101); return { "terminate": isCanceled(), "input": ""}; } // ============================================================================= function askForExport(exportfolder, tag) { var form = { "tag_label" : { "type" : "text", "label" : "append to filename", "just" : "l", "bounds" : { "x": 20, "w" : 160, }, }, "tag_toggle": { "type": "togglebox", "bounds" : { "y": -1, "x": 310, "w" : 25, }, "label" : "", "toggle" : true }, "tag": { "type": "texteditor", "bounds" : { "y" : -1, "x" : 350, "w" : 170, }, "multiline" : false, "wordwrap" : false, "default" : tag }, "path_label" : { "type" : "text", "label" : "use source folder", "just" : "l", "bounds" : { "x": 20, "w" : 160, }, }, "path_toggle": { "type": "togglebox", "bounds" : { "y": -1, "x": 310, "w" : 25, }, "label" : "", "toggle" : true }, "custompath_label" : { "type" : "text", "label" : "folder", "just" : "l", "bounds" : { "x": 20, "w" : 160, }, "visible": false, }, "custompath" : { "type" : "fileselect", "path" : exportfolder, "editable" : false, "dir" : true, "saving" : true, "label" : "empty", "bounds" : { "y": -1, "x": 180, "w" : 340, }, "visible": false, }, "spacer1" : { "type" : "text", "label" : " ", "just" : "r", "bounds" : { "x": 120, "w" : 200, "h" : 100 }, }, "fix" : { "type" : "button", "label" : "fix", "bounds" : { "x": 310, "w" : 100 }, "returns" : 1 }, "skip" : { "type" : "button", "label" : "skip", "bounds" : { "y": -1, "x": 420, "w" : 100, }, "returns" : 0 } }; var text = "Some files did not validate.\nDo you want to try to fix them?"; return dialog(header.title, text, "w", form); } // ============================================================================= function dialog_callback(props) { if (props["name"] == "path_toggle") var r = dialog({ "custompath" : { "visible" : !props["toggle"] }, "custompath_label" : { "visible" : !props["toggle"] } }); if (props["name"] == "tag_toggle") var r = dialog({ "tag" : { "visible" : props["toggle"] }, }); }