header={ "chef": "BeatRig", "dependencies": "ffmpeg,afconvert,ffprobe", "title": "Album Master", "description": "Create music master deliverables.", "instructions": "", "recipe_version": "0.372", "tags": "Audio", "type": "default", "os": "windows,macOS", "palette": "Clean Slate", "spice": "BQ==:G+Hy36kJcnQQYYavb/JV2Il6eCYr6h4hRJHCF9rRbhX5BcazL6cuTgUMdz0xA50VWA5PmmV0Q7aWkiUtNFtbk991l+t0e8IBXMReMIT1GNjnZZDv7n2YRAo0xcSQbY0ixjG6LLH4wGKnspKAoA/v69iLn6N2IH0XzIZwTGdHSo4=", "flavour": "ukFI2pecbv5MisUtfFWR6HeDWBbNapDeqfF9VQJjRxM2c60oq4RROmBdKYWvfmiJu3RpwwMQCknxYEtcf8w+/mONXVeSAkFpEHWTn5kmarjsoe/zXUlnUyYMMSN5xVbFadKffuH5fV2qIGl2SQ3cAXE5ArJu2BaLYDQ/GCxdWgc=", "time": 1723863789, "core_version": "0.6.4", "magnetron_version": "1.0.317", "functions": "main", "uuid": "a7b150f1d3e14bc7b3da4baecfc491c7", "ignore_dependencies": "afconvert" }; // GLOBAL FOR THE DURATION OF THE CURRENT FILE var duration = 0; // GLOBAL TO INDICATE IF FFMPEG CAN HANDLE LIBSOXR RESAMPLING var libsoxr = false; // GLOBAL TO INDICATE IF AFCONVCERT IS AVAILABLE var afconvert = false; // ============================================================================= function main() { // ------------------------------------ // CHECK IF FFMPEG IS AVAILABLE if(getAllowedApps("ffmpeg") == ''){ setMainMessage(""); abort("Could not start FFMPEG. Install it by from the settings." ); } // ------------------------------------ // CHECK IF FFPROBE IS AVAILABLE if(getAllowedApps("ffprobe") == ''){ setMainMessage(""); abort("Could not start FFPROBE. Install it by from the settings." ); } // ------------------------------------ // CHECK IF FFMPEG CONTAINS LIBSOXR var libsoxr = cmd("ffmpeg", ['-version']); libsoxr = containsString(libsoxr, '--enable-libsoxr') ? true : false; // ------------------------------------ // CHECK IF AFCONVERT IS AVAILABLE if( getAllowedApps("afconvert") != '' ){ afconvert = true; } else{ // TRY TO INSTALL if (gvar.isWindows == 0) { var loc = gvar.pss+'usr'+gvar.pss+'bin'+gvar.pss+'afconvert'; if(fileExists(loc)){ setAllowedApps('afconvert', loc); afconvert = true; } } } // ------------------------------------ // LOUDNES SPECS var album_max_lufs = -99.; var album_max_tp = -99.; var adjust = 0.; var limiter = false; var fileTypes = [ 'wav', 'aif', 'aiff', ]; // ------------------------------------ setMainMessage("analysing"); setProgress(0); // ------------------------------------ // CLEANUP THE LIST AND CHECK EXTENSION var files = getFiles(); var failed = 0; for (i=0;i 0){ dialog("Could not process", "One or more files could not be analysed.","w"); break; } // ------------------------------------ // GET ALL FILES FROM THE APP var files = getFiles(); // CHECK IF THERE IS AT LEAST ONE FILE if(files.length < 1){ dialog("No files ","Add files before running the recipe.","w"); setMainMessage(""); break; } // ------------------------------------ // START PATH FOR DIALOG var path = getPathInfo( files[0].path ); // ------------------------------------ // DIALOG var r = showDialog(path.folder); // ------------------------------------ // IF CANCELLED if(r.cancel){ setMainMessage(""); break; } // ------------------------------------ // ANALYSE LOUDNESS OF ALL AUDIO FILES INDIVIDUALLY var args = { "AdjustTargetType": "LUFS", "AdjustTargetLevel": 0. }; // ------------------------------------ // ANALYSE FILES var maxLoudFile = 0; var maxPeakFile = 0; for (i=0;i album_max_lufs) maxLoudFile = i; if(fileLevels.TruePeak > album_max_tp) maxPeakFile = i; album_max_lufs = Math.max(fileLevels.LUFS, album_max_lufs ); album_max_tp = Math.max(fileLevels.TruePeak, album_max_tp ); // SET STATUS files[i]['file_icon_color'] = "FFBBBBBB"; files[i]['file_icon'] = "square"; files[i]['file_status'] = ''; // LEVELS files[i]['lufs'] = Math.round( fileLevels.LUFS * 100. ) / 100.; files[i]['peaks'] = Math.round( fileLevels.TruePeak * 100. ) / 100.; files[i]['plr'] = Math.round( fileLevels.PeakLoudness * 100. ) / 100.; // SPECS files[i]['duration'] = specs.duration; var durationHuman = humanTime(specs.duration); files[i]['durationHuman'] = durationHuman.time; files[i]['codec'] = specs.streams[0].codec_name; files[i]['samplerate'] = specs.streams[0].sample_rate; files[i]['channels'] = specs.streams[0].channels; files[i]['bitrate'] = specs.streams[0].bits_per_sample; // CHECK SOURCE FILE files[i]['sourceSpecsValid'] = ( files[i].samplerate == 176400 && files[i].channels == 2 && files[i].codec == "pcm_s24le" && toLowerCase(path.ext) == 'wav' ? true : false); setFiles(files); } // ------------------------------------ // CALCULATE REF FILE AND ADJUSTMENT var refFile = maxLoudFile; adjust = -14. - album_max_lufs; // CHECK IF ADJUSTING IS NOT CAUSING CLIPPING if( album_max_tp + adjust > -1. ){ refFile = maxPeakFile; adjust = -1.1 - album_max_tp; } // UPDATE LIST var files = getFiles(); files[refFile]['file_icon_color'] = "FFBBBBBB"; files[refFile]['file_icon'] = "square"; files[refFile]['file_status'] = '(REF. FILE)'; setFiles(files); // ------------------------------------ // SET THE TOOLTIPS var files = getFiles(); for (i=0;i 0 && duration > 0.) { var progress = searchRegEx(cmd_output , "(?:time=)(.*?)(?:\ bitrate=)", "i", 1); progress = progress[progress.length-1].split(':'); progress = (parseFloat(progress[0]) * 60. * 60.) + (parseFloat(progress[1])*60.) + parseFloat(progress[2]); Math.round( parseFloat(progress)/ duration * 100. ); setProgress( progress ); // setMainMessage(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) }; } // ============================================================================= // FILE SPECS function filespecs(file) { setProgress(101); // ---------------------------------------- // GET VIDEO SPECS var ffprobe= cmd("ffprobe", [ '-v','error', '-show_entries','stream=codec_type,codec_name,channels,sample_rate,bits_per_sample : format=duration', '-i',file ]); var streams = []; // DURATION var duration = parseFloat ( searchRegEx(ffprobe, "(?:duration=)(.*?)(?=[\n\r])", "i", 1)[0] ); // UGGLY FIX BECAUSE replaceRegEx DOESNT KNOW HOW TO HANDLE NEW LINES // EXPLODE INTO STREAMS ffprobe = replaceRegEx(ffprobe, '[\n\r]', ',', "i"); var ffprobe_array = (getStringTokens(ffprobe, "\n\r")); // EXPLODE INTO STREAMS // STREAM DATA var ffprobe = searchRegEx(ffprobe, "(?:\\[STREAM\\],)(.*?)(?=,\\[\\/STREAM\\])", "i", -1); // LOOP OVER STREAMS for (var i = 0; i < ffprobe_array.length; i++) { // EXPLODE ENTRIES var entries = ffprobe_array[i].split(','); // STREAM DATA var stream = {}; for (var j = 0; j < entries.length; j++) { if (containsString(entries[j], "=")) { if (containsString(entries[j], "=") == false) continue; var l = entries[j].split('='); if ( l[0] == "codec_name" && l[0] != "unknown" ) stream = mergeObject( stream, { "codec_name" : l[1] }); else if ( l[0] == "codec_type" ) stream = mergeObject( stream, { "codec_type" : l[1] }); else if ( l[0] == "sample_rate" ) stream = mergeObject( stream, { "sample_rate" : l[1] }); else if ( l[0] == "channels" ) stream = mergeObject( stream, { "channels" : l[1] }); else if ( l[0] == "bits_per_sample" ) stream = mergeObject( stream, { "bits_per_sample" : l[1] }); } } // GROUP AUDIO AND VIDEO STREAMS if( stream.codec_type == "audio") { streams.push(stream); } } setProgress(0); // ---------------------------------------- return { 'streams' : streams, 'duration' : duration }; // ---------------------------------------- } // ============================================================================= function humanTime(totalSeconds) { var ms = (totalSeconds - Math.floor(totalSeconds))*1000; var totalMinutes = Math.floor(totalSeconds / 60); var seconds = Math.floor(totalSeconds % 60); var hours = Math.floor(totalMinutes / 60); var minutes = totalMinutes % 60; return { h: parseInt(hours), m: parseInt(minutes), s: parseInt(seconds), ms : parseInt(ms), time : pad(hours,2) + ":" + pad(minutes,2) + ":"+ pad(seconds,2) + "."+ pad(ms,3), }; } // ============================================================================= function pad(num, size) { num = toFixed(parseInt(num), 0); while (num.length < size) num = "0" + num; return num; } // ============================================================================ function dialog_callback(props) { // dummy }