header={ "chef": "BeatRig", "recipe_version": "1.6", "title": "Spot MXF Loudness Check", "description": "Validate the loudness of a MXF file against the SPOT recommendations", "spice": "BQ==:G+Hy36kJcnQQYYavb/JV2Il6eCYr6h4hRJHCF9rRbhX5BcazL6cuTgUMdz0xA50VWA5PmmV0Q7aWkiUtNFtbk991l+t0e8IBXMReMIT1GNjnZZDv7n2YRAo0xcSQbY0ixjG6LLH4wGKnspKAoA/v69iLn6N2IH0XzIZwTGdHSo4=", "flavour": "aJJn/8Z8ibOF2RjcJMkHZj9hWY9Crix2qyp+qWfFuPNQCS17I2lEj7g91g4FqJOBZ/z7wPLk88K/QFq+z1sVKL5KnhydlumV69TvohuYJa6FpbsU5sSStHp90z5uGaxNdqMhtYzJ3khkdlZT+7r9AGWnMlPZDHt2IQ2PoxfSG3E=", "time": 1724032852, "magnetron_version": "1.0.317", "palette": "Clean Slate", "core_version": "0.6.4", "dependencies": "ffmpeg,ffprobe", "type": "demo", "os": "windows,macOS", "functions": "main", "uuid": "47a4e062ea124d6d8fb57a24ea272d0f", "tags": "SPOT-MXF", "instructions": "Drop file(s) here" }; // ============================================================================= // GLOBALS var duration = 0; // ============================================================================= // MAIN function main() { // ----------------------------- ffmpegAllowed(); // ----------------------------- setProgress(0); // ----------------------------- // GET ALL FILES FROM THE APP var files = getFiles(); // ----------------------------- // CLEAN UP THE FILE LIST for (i=0;i= -22.5) { err++; msg.push("Loudness is too high ("+toFixed(stereo.LUFS, 1)+"LUFS)"); } else if(stereo.LUFS <= -23.5) { wrn++; msg.push("Low loudness("+toFixed(stereo.LUFS, 1)+"LUFS)"); } else { msg.push("The stereo stream is "+toFixed(stereo.LUFS, 1)+"LUFS."); } // VALIDATE PEAK LEVELS if(stereo.TruePeak > -1.) { err++; msg.push("Peak level exceeds -1dbFS ("+toFixed(stereo.TruePeak, 1)+"dB)"); } else { msg.push("The stereo stream peaks at "+toFixed(stereo.TruePeak, 1)+"dBTP."); } // VALIDATE MaxS LEVELS if(stereo.maxS > 5.) { wrn++; msg.push("MaxS exceeds 5LU ("+toFixed(stereo.maxS, 1)+"LU)"); } else { msg.push("The MaxS for the stereo stream is ("+toFixed(stereo.maxS, 1)+"LU)"); } // ------------------------------------ // SURROUND STREAM msg.push("- - - - - - - - - -"); if(surround.LUFS > -70.0) { hasSurround = 1; // VALIDATE LOUDNESS if(surround.LUFS >= -22.5){ err++; msg.push("Loudness is out of range ("+toFixed(surround.LUFS, 1)+"LUFS)"); } else if(surround.LUFS <= -23.5){ wrn++; msg.push("Low loudness("+toFixed(surround.LUFS, 1)+"LUFS)"); } else{ msg.push("The surrond stream is "+toFixed(stereo.LUFS, 1)+"LUFS."); } // VALIDATE PEAK LEVELS if(surround.TruePeak > -1.){ err++; msg.push("Peak level exceeds -1dbFS ("+toFixed(surround.TruePeak, 1)+"dBTP)"); } else{ msg.push("The surround stream peaks at "+toFixed(surround.TruePeak, 1)+"dBTP."); } // VALIDATE MaxS LEVELS if(surround.maxS > 5.) { wrn++; msg.push("MaxS exceeds 5LU ("+toFixed(surround.maxS, 1)+"LU)"); } else { msg.push("The MaxS for the surround stream is ("+toFixed(surround.maxS, 1)+"LU)"); } } else { msg.push("The MXF has no surround audio."); } } // UPDATE files[i]["file_status"] = "Valid R128 file"+(hasSurround ? "" : "; without surround" ); // -- warning if(wrn > 0) { files[i]["file_icon_color"] = "FFee4626"; files[i]["file_status"] = "The file validates with warnings"; } // -- error if(err > 0) { files[i]["file_icon"] = "ExclamationCircle"; files[i]["file_icon_color"] = "FFff0000"; files[i]["file_status"] = "The file did not validate"; } // -- all done files[i]["file_tooltip"] = msg.join('\n'); setFiles(files); } // ALL DONE setProgress(100); setMainMessage(" "); } // ============================================================================= // CHECK IF FFMPEG IS AVAILABLE AND VALID // ----------------------------------------------------------------------------- function ffmpegAllowed() { if(getAllowedApps("ffmpeg") == '' || searchRegEx(cmd('ffmpeg', ["-version"]), "ffmpeg version", "i", 0).length == 0) abort("FFMPEG is not available"); } // ============================================================================= // ffmpegEBUR128() // helper function for parsing EBUR128 specs returned by ffmpeg // ANALYSE LOUDNESS USING FFMPEG EBUR128 LIB (LOUDNORM IS // INACCURATE FOR SHORT CONTENT!) // ----------------------------------------------------------------------------- function ffmpegEBUR128(file, stream) { // DEFAULT RETURN VALUES var rtn = { "LUFS" : -99., "TruePeak" : -99., "maxM" : -99., "maxS" : -99., }; // SELECT STREAM if(stream == "stereo") { amix = "[a:0][a:1]amerge=inputs=2[amix];"; } else if(stream == "surround") { amix = "[a:2][a:3][a:4][a:5][a:6][a:7]amerge=inputs=6[amix];"; } else { return rtn; } // FFMPEG CALL var ffmpeg = cmd("ffmpeg", [ '-i',file, '-nostats', '-filter_complex',amix+'[amix]ebur128=peak=sample', '-f','null', '-', ]); // GET THE INTEGRATED LOUDNESS FROM THE FFMPEG RETURN // THE LAST INTEGRATED LOUDNESS (IT IS SHOWN CUMULATIVE) AND FORCE AS FLOAT var loud_i = searchRegEx(ffmpeg, "(?:I:)(.*)(?=LUFS)", "i", -1); rtn.LUFS = parseFloat(loud_i[loud_i.length-1]); // GET THE TRUE PEAK FROM THE FFMPEG RETURN // THE LAST TRUE PEAK (IT IS SHOWN CUMULATIVE) AND FORCE AS FLOAT var loud_peak = searchRegEx(ffmpeg, "(?:Peak:)(.*)(?=dBFS)", "i", -1); rtn.TruePeak = parseFloat(loud_peak[loud_peak.length-1]); // THERE IS NO MAX S FUNCTION IN FFMPEG EBUR128, BUT S IS SHOWN // WE NEED TO RUN THOURGH THE VALUES AND CHECK THE MAX OUR SELVES var loud_s = searchRegEx(ffmpeg, "(?:S:)(.*)(?=I:)", "i", -1); for (var i = 0; i < loud_s.length; i++) rtn.maxS = Math.max( loud_s[i], rtn.maxS ); rtn.maxS = rtn.maxS - rtn.LUFS; // THERE IS NO MAX M FUNCTION IN FFMPEG EBUR128, BUT M IS SHOWN // WE NEED TO RUN THOURGH THE VALUES AND CHECK THE MAX OUR SELVES var loud_m = searchRegEx(ffmpeg, "(?:M:)(.*)(?=S:)", "i", -1); for (var i = 0; i < loud_m.length; i++) rtn.maxM = Math.max( loud_m[i], rtn.maxM ); rtn.maxM = rtn.maxM - rtn.LUFS; return rtn; } // ============================================================================= // FFPROBE SPECS PARSER function ffprobeStreamSpecs(file) { // RETURN VALUES var rtn = []; // FFPROBE COMMAND var ffprobe = cmd("ffprobe", [ '-v','error', '-select_streams','a', '-show_entries',' stream=codec_type,codec_name,bit_rate,channels,sample_rate,bits_per_sample : format=duration,nb_streams ', '-of','default=noprint_wrappers=1', '-i',file ]); // DURATION IS ACTUALLY NOT A STREAM BUT A FILE THING // SO ONLY MEASURED ONCE AND NOT PER STREAM duration = parseFloat(searchRegEx(ffprobe, "(?:duration=)(.*?)(?=[\n\r])", "o", -1)[0]); // RETURN VALUES var streamCount = searchRegEx(ffprobe, "(?:codec_type=)(.*?)(?=[\n\r])", "o", -1); streamCount = parseInt(streamCount.length); for (j=0;j 0 && duration > 0){ var progress = searchRegEx(cmd_output , "(?:t:)(.*?)(?:\ TARGET:)", "i", -1); progress = progress[progress.length-1]; progress = parseFloat(progress)/ duration * 100.; progress = Math.round(progress); setProgress(progress); } }