header={ "chef": "BeatRig", "recipe_version": "1.52", "title": "Spotify Ad Studio Check", "description": "Validate an audio file conform the Spotify Ad Studio specifications", "spice": "BQ==:G+Hy36kJcnQQYYavb/JV2Il6eCYr6h4hRJHCF9rRbhX5BcazL6cuTgUMdz0xA50VWA5PmmV0Q7aWkiUtNFtbk991l+t0e8IBXMReMIT1GNjnZZDv7n2YRAo0xcSQbY0ixjG6LLH4wGKnspKAoA/v69iLn6N2IH0XzIZwTGdHSo4=", "flavour": "qRQAzp91fHC/G5UH3KyyELXLPyNIObc46d9tXJ03VJDIWWo0linHOProhPchSxqsHe+Ws/bROS2RUgfK2XBwXLMTv3sD9tMYtQhta+FuORQ57Thj1gVorDJS852c9ziZ3zVTyik10A+eA2Qf7oljJ7tTojrmMnDtd1mbohQRJGQ=", "time": 1703177311, "magnetron_version": "1.0.279", "tags": "audio,spotify", "core_version": "0.6.2", "palette": "Clean Slate", "dependencies": "ffmpeg,ffprobe", "type": "default", "os": "windows,macOS", "functions": "main", "uuid": "13e85c35d46847ba98116e2d036a9782", "instructions": "Drop file(s) here" }; // ============================================================================= // MAIN function main() { var settings = { "RelativeGate": -10., "DialogGate": false, "CalibrationLU": -16.0, "tag" : '-SPOTIFY' }; // ----------------------------- setProgress(0); // ----------------------------- // 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"] = "ExclamationTriangle"; files[i]["file_icon_color"] = "FFff0000"; files[i]["file_status"] = "File cannot be fixed"; } // WARNINGS THAT CAN BE FIXED else if( validate.wrn > 0 ) { files[i]["file_icon"] = "ExclamationTriangle"; files[i]["file_icon_color"] = "FFff8800"; files[i]["file_status"] = "Needs to be fixed"; files[i]["process_adjust"] = validate.gain; needsfixing.push(i); } // ALL GOOD else{ files[i]["file_icon"] = "CheckSquare"; files[i]["file_icon_color"] = "FF008800"; files[i]["file_status"] = "Valid"; } // update list files[i]["file_tooltip"] = validate.msg; setFiles(files); } // ALL DONE setProgress(100); setMainMessage(" "); // ----------------------------- // 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 || validate.wrn > 0){ files[fileid]['file_icon'] = "exclamation"; files[fileid]['file_icon_color'] = "FFFF8800"; files[fileid]["file_tooltip"] = validate.msg; 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"] = validate.msg; files[fileid]['file_status'] = "The file was fixed"; } // setFiles(files); } } } // ----------------------------- setProgress(100); setMainMessage("Done"); } // ============================================================================= // CALLBACK LOUDNESS ANALYSING // this function gets called when busy LevelFileAnalyse. progress 0. to 1. function LevelFileAnalyse_Progress(file, progress) { var files = getFiles(); setProgress((i+progress)/files.length*100); } // ============================================================================= // CMD CALLBACK (FFMPEG / FFPROBE) // this function is called about once a second during a cmd() call function cmd_callback(cmd_name, cmd_output){ } // ============================================================================= function getSpecs(file) { var ffprobe = cmd("ffprobe", [ '-loglevel','error', '-show_entries','format=size : stream=codec_name,channels,channel_layout,sample_rate,bit_rate : format=duration', // '-show_format', //'-of','csv=p=0', '-of','default=noprint_wrappers=1', file, ]); return { 'codec' : searchRegEx(ffprobe, "(?:codec_name=)(.*?)(?=[\n\r])", "o", 1)[0], 'channels' : parseInt(searchRegEx(ffprobe, "(?:channels=)(.*?)(?=[\n\r])", "o", 1)[0]), 'channel_layout' : searchRegEx(ffprobe, "(?:channel_layout=)(.*?)(?=[\n\r])", "o", 1)[0], 'duration' : parseFloat(searchRegEx(ffprobe, "(?:duration=)(.*?)(?=[\n\r])", "o", 1)[0]), 'samplerate' : parseInt(searchRegEx(ffprobe, "(?:sample_rate=)(.*?)(?=[\n\r])", "o", 1)[0]), 'bitrate' : parseInt(searchRegEx(ffprobe, "(?:bit_rate=)(.*?)(?=[\n\r])", "o", 1)[0]), 'filesize' : parseInt(searchRegEx(ffprobe, "(?:size=)(.*?)(?=[\n\r])", "o", 1)[0]), } } // ============================================================================= function validateFile(specs, levels) { var gain = 0; var wrn = 0; // warnings van be fixed var err = 0; // errors can not be fixed var msg = []; // messages // -------------------------------- // ERRORS // -------------------------------- // CHANNELS if(specs.channels != 2 /* || specs.channel_layout != "stereo" */){ err++; msg.push("File is not stereo ("+specs.channels+" channels "+specs.channel_layout+")"); } // DURATION if(specs.duration > 30.05){ err++; msg.push("File is too long ("+specs.duration+"sec)"); } // TOO SHORT //if(specs.duration < 15.){ // err++; // msg.push("File is too short ("+specs.duration+"sec)"); // } // -------------------------------- // WARNINGS // -------------------------------- // CODEC if(specs.codec != "mp3"){ wrn++; msg.push("File is not a MP3 ("+specs.codec+")"); } // FILESIZE if(specs.filesize >= 1048576){ wrn++; msg.push("File is too big ("+toFixed(specs.filesize/1048576., 2)+"MB)"); } // SAMPLE RATE if(specs.samplerate != 44100){ wrn++; msg.push("File is not 44kHz sample rate ("+(specs.samplerate/1000.)+"kHz)"); } // bitrate if(specs.bitrate < 192000){ wrn++; msg.push("Bitrate is too low ("+(specs.bitrate/1024.)+"kbps)"); } // VALIDATE LOUDNESS if(levels.LUFS >= -14.5){ wrn++; msg.push("File is too loud ("+toFixed(levels.LUFS, 1)+"LUFS)"); } // VALIDATE LOUDNESS if(levels.LUFS <= -17.5){ wrn++; msg.push("File is too soft ("+toFixed(levels.LUFS, 1)+"LUFS)"); } // VALIDATE PEAK LEVELS if(levels.TruePeak > -2.){ wrn++; msg.push("Peak level exceeds -2dbFS ("+toFixed(levels.TruePeak, 1)+"dB)"); } // ADJUST LEVELS /* echo(">>>> LU: "+levels.LUFS); echo(">>>> preak: "+levels.TruePeak); echo(">>>> gain: "+ (-16. - levels.LUFS) ); echo(">>>> if: "+ ((levels.TruePeak + gain) > -2.) ); echo(">>>> and: "+ ( levels.LUFS + maxPeakGain > -14.5 ) ); echo(">>>> then: "+ ( maxPeakGain = -2. - levels.TruePeak ) ); */ gain = ( adjLU = -16. - levels.LUFS ); if( (levels.TruePeak + gain) > -2.) { var maxPeakGain = -2. - levels.TruePeak; if ( (levels.LUFS + maxPeakGain) > -14.5 ){ gain = maxPeakGain; msg.push("The file is a little dynamic, and normalised to " + (levels.LUFS + maxPeakGain) + " to prevent clipping." ); } else{ err++; msg.push("The file is too dynamic and cant be fixed. ("+toFixed(levels.LUFS,1)+'LUFS / '+toFixed(levels.TruePeak,1)+'dBTP)'); } } // RETURN return{ "gain" : gain, "err" : err, "wrn" : wrn, "msg" : (msg.length > 1 ? "- " : "")+msg.join('\n- '), } } // ============================================================================= 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"] }, }); }