header={ "recipe_version": "1.1347", "title": "Replace Audio in Video", "description": "Replace audio in video", "tags": "default", "chef": "BeatRig", "dependencies": "ffmpeg,ffprobe", "spice": "BQ==:G+Hy36kJcnQQYYavb/JV2Il6eCYr6h4hRJHCF9rRbhX5BcazL6cuTgUMdz0xA50VWA5PmmV0Q7aWkiUtNFtbk991l+t0e8IBXMReMIT1GNjnZZDv7n2YRAo0xcSQbY0ixjG6LLH4wGKnspKAoA/v69iLn6N2IH0XzIZwTGdHSo4=", "flavour": "sSNGU2/36UC66XCtfSVNWg8mpXD5OPv/eJNZ0gvG9X9Ud49gXnu/qtbiRQXTpi9w5qwwhJvM/HDIDz4eYFFLyUQZO13bu+mSixKeOhl4/W1o7hEG2ywjbA3KVhFQlMGFni0AoItrRjULl2djL+7bbXRbZlG7H5Pyg27OExW5ST0=", "time": 1724010594, "core_version": "0.6.4", "magnetron_version": "1.0.317", "functions": "main,onConfig,onAbout", "uuid": "9b234202dc46424d9b3765c0e0a411a7", "instructions": "", "type": "default", "os": "windows,macOS", "palette": "Clean Slate" }; // ============================================================================= // GLOBALS var framecount = 0; // GLOBAL AVAIL. FOR PROGRESS BAR DURING PROCESSING var settings = {}; // SETTINGS var settingsFile = ""; // LOCATION OF THE SETTINGS FILE var cue = []; // LIST WITH FILES THAT WILL BE PROCESSED // ============================================================================= // SET DEFAULT VALUES function init() { // LOCATION OF THE SETTINGS FILE settingsFile = folders.content+gvar.pss+'default.txt'; // SETTINGS settings = { "preset" : 'original', // "sourcename" : 'video', // Use the audio or video name as source for the new file "customFolder" : false, // Move fixed files to a custom folder "customFolder_path" : folders.home, // By default this is the users home folder "nametag" : true, // Tag added to fixed file "nametag_val" : '-RELAYED', "subFolder" : false, // Move fixed files to a subfolder "subFolder_val" : 'RELAYED', }; // } // ============================================================================= function main() { // ----------------------------- // START THE MACHINE setProgress(0); // ----------------------------- // DEFAULT SETTINGS init(); // ----------------------------- // CUSTOM SETTINGS loadSettings(); // ----------------------------- // CURRENT FILES var cur_v = false; var cur_a = false; var msg = []; var r = { video: '', audio: '', offset_enabled: false, offset: 0, add_enabled: true, }; // ----------------------------- // KEEP GOING BACK TO THE DIALOG WHILE THERE ARE ERRORS // AND NOT PRESSED CANCEL while (1) { // if(cur_a == false || cur_v == false){ r.add_enabled = false; } // SHOW THE DIALOG r = showDialog(r, msg); // ----------------------------- // WHEN CANCELLED WAS PRESSED STOP if (r.cancel) { break; } // ENABLE THE ADD BUTTON BY DEFAULT r.add_enabled = true; // RESET MESSAGES msg = []; // AN EMPTY FILEPATH IS RETURNING A / SO MAKE SURE TO SET IT TO EMPTY r.video = (r.video == '/' ? '' : r.video ); r.audio = (r.audio == '/' ? '' : r.audio ); // ----------------------------- // AN AUDIO OR VIDEO FILE HAS BEEN SELECTED if(r.returns == 9){ // GET VIDEO FILE SPECS if(r.video != "" && cur_v.path != r.video ){ cur_v = getVideoSpecs(r.video); if(cur_v == false) { msg.push('Could not read video file.'); } } // GET AUDIO FILE SPECS if(r.audio != "" && cur_a.path != r.audio ){ cur_a = getAudioSpecs(r.audio); if(cur_a == false) { msg.push('Could not read audio file.'); } } } // ----------------------------- // IF AUDIO AND VIDEO ARE SET, COMPARE LENGTH OF BOTH if(cur_a && cur_v){ // IF AUDIO AND VIDEO ARE NOTE THE SAME LENGTH if(cur_a.duration > cur_v.duration){ r.add_enabled = false; msg.push('The audio file is longer then the video file'); } else if(cur_a.duration < cur_v.duration){ r.offset_enabled = true; r.offset_max = parseInt( (cur_v.duration - cur_a.duration) * cur_v.framerate ); r.offset_help = 'The video is '+r.offset_max +" frames longer then the audio."; } else{ r.offset_enabled = false; r.offset = 0; } } // ----------------------------- // RE-OPEN THE DIALOG WHEN A VIDEO OR AUDIO FILE WAS CHANGED // AN NOT A BUTTON WAS PUSEHD if(r.returns == 9 ) { continue; } // ----------------------------- // ADD THE FILE if(r.returns == 7 && cur_a && cur_v){ var output_file = filename(); cue.push({ audio_file: cur_a.path, // audio file video_file: cur_v.path, // video file output_file: output_file, // output file offset: r.offset, // audio offset in frames framerate: cur_v.framerate, // framerate, to calc offset in sec framecount: cur_v.framecount, // framecount, for progress }); // RESET DIALOG r.video = ''; r.audio = ''; r.add_enabled = true; r.offset = 0; r.offset_max = 0; r.offset_enabled = false; // RESET AUDIO/VIDEO FILE cur_a = false; cur_v = false; // RESET MESSAGES msg = []; } // ----------------------------- // WHEN OK IS CLICKED if(r.returns == 1){ break; } } // ----------------------------- // STOP THE ENTIRE RECIPE if (r.cancel) { break; } // ----------------------------- // START PROCESSING // CURRENT FILE LIST var files = getFiles(); // LOOP CUE for (var i = 0; i < cue.length; i++) { var f = cue[i]; // SET FRAMECOUNT FOR PROGRESS BAR framecount = parseInt(f.framecount); // ----------------------------- // CHECK IF FILE ALLREADY EXISTS, CAN ONLY HAPPEN IF // IT WAS CREATED AFTER ADDING IT TO THE LIST if( fileExists(f.output_file) ){ continue; } // ----------------------------- // CHECK IF FOLDER EXISTS var path = getPathInfo(f.output_file); path = path.folder; path = path.split(gvar.pss); // path to array path.remove(""); // remove empty entries var test = gvar.isWindows ? path[0] : ''; for (var j = gvar.isWindows ? 1 : 0; j < path.length; j++) { test = test + gvar.pss + path[j]; if(fileExists(test)){ continue; } makeFolder( test ); } // ----------------------------- // FFMPEG setProgress(0); setMainMessage("Processing"); // video file var ffmpeg = [ '-i',f.video_file, ]; // audio offset if(f.offset > 0){ ffmpeg = concatArray(ffmpeg, [ '-itsoffset',parseFloat(f.offset) / parseFloat(f.framerate), ]); } // audio file ffmpeg = concatArray(ffmpeg, [ '-i',f.audio_file, ]); // AUDIO & VIDEO MAPPING ffmpeg = concatArray(ffmpeg, [ '-map','0:0', '-map','1:0', ]); if(settings.preset == 'original') { ffmpeg = concatArray(ffmpeg, [ '-c:v','copy', '-c:a','copy', ]); } else if(settings.preset == 'small mp4') { var properties = levelFileAnalyse(f.audio_file, {"AdjustTargetType" : "Peak", "AdjustTargetLevel" : -1.0}); var adjust = Math.round (properties.AdjustLevel*100)/100; ffmpeg = concatArray(ffmpeg, [ '-c:v','libx264', '-preset','slow', '-crf','32', // '-b:v','800k', // '-maxrate','16M', // '-bufsize','16M', //'-pass','4', //'-vf','format=yuv420p', //'-profile:v','baseline', //'-level:v','4.0', //'-qcomp','16', //'-qmin','1', //'-qmax','25', '-c:a','aac', '-b:a','128k', '-filter:a','volume='+adjust+'dB', ]); } else if(settings.preset == 'large mp4') { var properties = levelFileAnalyse(f.audio_file, {"AdjustTargetType" : "LUFS", "AdjustTargetLevel" : -14.0}); var adjust = Math.round (properties.AdjustLevel*100)/100; ffmpeg = concatArray(ffmpeg, [ '-c:v','libx264', '-preset','slow', '-crf','23', // '-b:v','16M', // '-maxrate','16M', // '-bufsize','16M', //'-pass','4', //'-vf','format=yuv420p', //'-profile:v','high', //'-level:v','4.0', //'-qcomp','16', //'-qmin','1', //'-qmax','25', '-c:a','aac', '-b:a','256k', '-filter:a','volume='+adjust+'dB', ]); } // OUTPUT FILE ffmpeg = concatArray(ffmpeg, [ f.output_file, '-y' ]); // DO YA THING echo(cmd("ffmpeg", ffmpeg)); setProgress(100); // CHECK IF FILE WAS CREATED if( fileExists(f.output_file) ){ files.push({ path: f.output_file, file_icon : "CheckSquare", file_icon_color : "FF008800", file_status : "Done", }); setFiles(files); } else{ files.push({ path: f.video_file, file_icon : "CheckSquare", file_icon_color : "FF880000", file_status : "Failed", }); setFiles(files); } } // ----------------------------- // ALL DONE setMainMessage("-"); setProgress(0); return true; } // ============================================================================ // DIALOG CALLBACK function dialog_callback(props) { // echo("dialog_callback " + objectToString(props)); } // ============================================================================ // DIALOG CALLBACK function dialog_settings_callback(props) { // echo("dialog_settings_callback " + objectToString(props)); // ---------------- // UPDATE SETTINGS if (props["name"] == "preset") { settings['preset'] = props["text"]; } if (props["name"] == "sourcename") { settings['sourcename'] = props["text"]; } if (props["name"] == "customFolder") { settings['customFolder'] = props["toggle"]; } if (props["name"] == "customFolder_path") { settings['customFolder_path'] = props["path"]; } if (props["name"] == "nametag") { settings['nametag'] = props["toggle"]; } if (props["name"] == "nametag_val") { settings['nametag_val'] = props["text"]; } if (props["name"] == "subFolder") { settings['subFolder'] = props["toggle"]; } if (props["name"] == "subFolder_val") { settings['subFolder_val'] = props["text"]; } // ---------------- // UPDATE DIALOG dialog({ "preset" : { "default" : settings['preset'] }, "files" : { "default" : settings['files'] }, "customFolder_path" : { "visible" : settings['customFolder'] }, "subFolder_val" : { "visible" : settings['subFolder'] }, "nametag_val" : { "visible" : settings['nametag'] }, }); // ---------------- } // ============================================================================ // WHEN A VIDEO OR AUDIO FILE IS SET, // CLOSE THE DIALOG SO IT CAN BE ANALYSED // THE DIALOG WILL BE RE-OPENED WHEN DONE function dialog_add_callback(props) { if ((props["name"] == "video" || props["name"] == "audio") && props["path"] != '') { var r = dialog({ "quit" : { "type" : "return", "value" : 9 }, }); break; } // ---------------- } // ============================================================================= // FFMPEG PROCESS STATUS function cmd_callback(cmd_name, cmd_output) { // THE DIALOG DURING PROCESSING if(cmd_name=="ffmpeg" && cmd_output.length > 0 && framecount > 0) { var progress = parseInt ( searchRegEx(cmd_output, "(?:frame=)(.*?)(?:fps=)", "i", 1)[0] ); progress = Math.round( progress/framecount* 100 ); setProgress(progress); } // ---------------- } // ============================================================================= function getVideoSpecs(file) { // GETFRAMECOUNT FOR PROGRESS setProgress(101); setMainMessage("Opening file"); var ffprobe = cmd("ffprobe", [ "-v","error", "-select_streams","v:0", "-show_entries","stream=nb_frames,duration,r_frame_rate", "-of","default=noprint_wrappers=1", // "default=noprint_wrappers=1:nokey=1", file, ]); // RETURN VALUES var rtn = { "path" : file, "duration" : parseFloat (searchRegEx(ffprobe, "(?:duration=)(.*?)(?=[\n\r])", "o", 1)[0] ), "framerate" : parseFloat (searchRegEx(ffprobe, "(?:r_frame_rate=)(.*?)(?=[\n\r])", "o", 1)[0] ), "framecount" : parseFloat (searchRegEx(ffprobe, "(?:nb_frames=)(.*?)(?=[\n\r])", "o", 1)[0] ), }; // TEST IF VALID FILE if(rtn.duration == 0){ rtn = false; } // setProgress(0); setMainMessage(""); // return rtn; } // ============================================================================= function getAudioSpecs(file) { // GETFRAMECOUNT FOR PROGRESS setProgress(101); setMainMessage("Opening file"); var ffprobe = cmd("ffprobe", [ "-v","error", "-select_streams","a:0", "-show_entries","stream=duration", "-of","default=noprint_wrappers=1", file, ]); // RETURN VALUES var rtn = { "path" : file, "duration": parseFloat (searchRegEx(ffprobe, "(?:duration=)(.*?)(?=[\n\r])", "o", 1)[0] ), }; // TEST IF VALID FILE if(rtn.duration == 0){ rtn = false; } // setProgress(0); setMainMessage(""); // return rtn; } // ============================================================================= // GENERATE FILENAME function filename() { // AUDIO AND VIDEO PATH INFO var v = getPathInfo(cur_v.path); var a = getPathInfo(cur_a.path); // SOURCE PATH/NAME CAN BE FROM THE AUDIO OR VIDEO SOURCE var source = (settings.sourcename == 'audio' ? a : v); // var name = source.basename; var ext = (settings.preset == 'original' ? v.ext : 'mp4'); // FILE EXTENSION var path = source.folder; var full = ''; // FULL PATH var inc = 0; // FILE INCREMENTAL FOR EXISTING FILES // CUSTOM FOLDER if(settings.customFolder){ path = settings.customFolder_path; } // SUB FOLDER if(settings.subFolder){ path = path+gvar.pss+settings.subFolder_val; } // NAME TAG if(settings.nametag){ name = name+settings.nametag_val; } // IF FILE EXISTS, ADD INCREMENTAL while (true) { // ADD INC. TO END OF THE NAME, INITIALY EMPTY var temp_name = name+(inc > 0 ? ' ['+inc+']' : ''); // FULL PATH INCL. FILENAME full = path+gvar.pss+temp_name+'.'+ext; // CHECK IF OUTPUT FILENAME IS UNIQUE var unique = true; for (var i = 0; i < cue.length; i++) { if( cue[i].output_file == full ){ unique = false; } } // IF FILE DOES NOT EXIST CONTINUE if(unique && !fileExists(full) ){ break; } // IF FILE EXISTS ADD INC. NUMBER inc++; } // RETURN THE FULL PATH return full; } // ============================================================================= // THE ACTUAL ADD FILE DIALOG function showDialog(r, msg) { // CUE LENGTH SO FAR var cue_count = cue.length; // FIELDS var args = {}; // ERR MESSAGE FIELD if(msg.length > 0){ mergeObject(args, { "message" : { "type" : "text", "label" : "ERROR:\n- "+msg.join('\n- '), "bounds" : {"x": 20, "w" : 530, "h" : 200 }, } }); } // ALL OTHER FIELDS mergeObject( args, { // HIDDEN VALUES, ONLY FOR LOOPING PURPOSE "offset_enabled": { "type": "texteditor", "visible" : false, "default" : r.offset_enabled, }, "offset_max": { "type": "texteditor", "visible" : false, "default" : r.offset_max, }, "add_enabled": { "type": "texteditor", "visible" : false, "default" : r.add_enabled, }, // FIELDS "spacer1" : { "type" : "text", "label" : "", "just" : "r", "bounds" : { "x": 20, "w" : 550, "h" : 1 }, }, "video_label" : { "type" : "text", "label" : "video file", "just" : "l", "bounds" : {"x": 20, "w" : 100 }, }, "video" : { "type" : "fileselect", "editable" : false, "dir" : false, "saving" : false, "wildcard" : "*.mov, *.mp4", "label" : "video file", "bounds" : { "y": -1, "x": 120, "w" : 430 }, "path" : r.video, }, "header_label" : { "type" : "text", "label" : "audio file", "just" : "l", "bounds" : { "x": 20, "w" : 100 }, }, "audio" : { "type" : "fileselect", "editable" : false, "dir" : false, "saving" : false, "wildcard" : "*.aif, *.aiff, *.wav", "label" : "audio file", "bounds" : { "y": -1, "x": 120, "w" : 430 }, "path" : r.audio, }, "offset_help" : { "type" : "text", "label" : r.offset_help, "bounds" : { "x": 20, "w" : 430 }, "visible" : r.offset_enabled, }, "offset_label" : { "type" : "text", "label" : "offset", "just" : "l", "bounds" : { "x": 20, "w" : 100 }, "enabled" : r.offset_enabled, "visible" : r.offset_enabled, }, "offset": { "type": "slider", "style": "slider_h", "visible": true, "range" : { "min": 0, "max" : r.offset_max, "interval" : 1, "decimals" : 0 }, "bounds" : { "y": -1, "x": 120, "w" : 430 }, "value": r.offset, "enabled" : r.offset_enabled, "visible" : r.offset_enabled, }, "add" : { "type" : "button", "label" : "add", "returns" : 7, "bounds" : { "x": 450, "w" : 100 }, "enabled" : r.add_enabled, }, "spacer2" : { "type" : "text", "label" : "", "just" : "r", "bounds" : { "x": 20, "w" : 530, "h" : 80 }, }, "list" : { "type" : "text", "label" : "There "+(cue_count == 1 ? "is [one] file": 'are '+(cue_count == 0 ? 'no' : "["+cue_count+"]")+' files')+" in the cue.", "just" : "l", "bounds" : { "x": 20, "w" : 330 }, }, "cancel" : { "type" : "button", "label" : "cancel", "returns" : 0, "bounds" : { "y": -1, "x": 340, "w" : 100 }, }, "ok" : { "type" : "button", "label" : "start", "returns" : 1, "bounds" : { "y": -1, "x": 450, "w" : 100 }, "enabled" : (cue_count > 0 ? true : false), }, }); // RETURN return dialog(args, 'dialog_add_callback'); } // ============================================================================= function LevelFileAnalyse_Progress(file, progress) { setProgress(100 * progress); } // ============================================================================= // ON CONFIG, SHOW DIALOG AND SAVE SETTINGS function onConfig() { // ----------------------------- // DEFAULT SETTINGS init(); // ----------------------------- // CUSTOM SETTINGS FROM FILE loadSettings(); // ----------------------------- // 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 } // ----------------------------- // NO SLASHES ARE ALLOWED settings.subFolder_val = replaceInString( settings.subFolder_val, gvar.pss, "" ); // ----------------------------- // CUSTOM FOLDER SHOULD NOT END WITH A SLASH settings.subFolder_val = ( settings.subFolder_val.charAt(myString.length - 1) == gvar.pss ? settings.subFolder_val.substring( 0 , myString.length-1) : settings.subFolder_val ); // ----------------------------- // SAVE SETTINGS writeFile( settingsFile, objectToString(settings) ); // ----------------------------- } // ============================================================================= // ON ABOUT, SHOW POPUP WITH BUTTON TO OPEN WEBSITE function onAbout() { // ----------------------------- // DEFAULT SETTINGS init(); // ----------------------------- var args = { "spacer_top": { "type": "text", "label": "","just": "c", "bounds": { "x": 20, "w": 550, "h": 20 }, }, "info": { "type": "texteditor", "bounds" : { "y" : 70, "x" : 20, "w" : 510, "h" : 150 }, "visible": true, "multiline" : true, "wordwrap" : true, "default" : 'More information on the recipes page on our website.', "enabled" : false }, "reveal_settings_file" : { "type" : "button", "label" : "reveal settings file", "bounds" : { "x" : 20, "w" : 510 }, "returns" : 2 }, "showpage" : { "type" : "button", "label" : "show page", "bounds" : { "x" : 20, "w" : 510 }, "returns" : 1 }, "close" : { "type" : "button", "label" : "close", "bounds" : { "x" : 20, "w" : 510 }, "returns" : 0 } }; var r = dialog(header.title, "" , "", args); if (r.returns == 2) revealPath(settingsFile); if (r.returns == 1) launchInBrowser('https://magnetron.app/recipes/uuid/'+header.uuid+'/'); } // ============================================================================= // LOAD DEFAULT VALUES function loadSettings() { // ADD/UPDATE SETTINGS if ( fileExists(settingsFile) ){ // GET FILE var t = stringToObject( readFile(settingsFile) ); // REPLACE VALUES ONE-BY-ONE for (var j = 0; j < getObjectSize(t); j++) { settings[getObjectKey(t, j)] = getObjectValue(t, j); } } // RETURN THE VALUES return true; } // ============================================================================= function showSettings() { var form = { "spacer_top": { "type": "text", "label": "","just": "c", "bounds": { "x": 20, "w": 550, "h": 20 }, }, "preset_label" : { "type" : "text", "label" : "Preset", "just" : "l", "bounds" : { "x": 20, "w" : 120, }, }, "preset" : { "type" : "combobox", "default" : settings.preset, "items" : "original,small mp4,large mp4", "bounds" : { "y": -1, "x" : 180, "w" : 370, }, }, "customFolder_label" : { "type" : "text", "label" : "Custom folder", "just" : "l", "bounds" : { "x": 20, "w" : 120, }, }, "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" : 370, }, "visible": false, }, "subFolder_label" : { "type" : "text", "label" : "Sub folder", "just" : "l", "bounds" : { "x": 20, "w" : 120, }, }, "subFolder": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.subFolder }, "subFolder_val": { "type": "texteditor", "bounds" : { "y": -1, "x" : 180, "w" : 370, }, "multiline" : false, "wordwrap" : false, "default" : settings.subFolder_val, "visible": true, }, "sourcename_label" : { "type" : "text", "label" : "Source name", "just" : "l", "bounds" : { "x": 20, "w" : 120, }, }, "sourcename" : { "type" : "combobox", "default" : settings.sourcename, "items" : "audio,video", "bounds" : { "y" : -1, "x": 180, "w" : 370, }, "returns" : 0, }, "nametag_val_label" : { "type" : "text", "label" : "Name tag", "just" : "l", "bounds" : { "x": 20, "w" : 120, }, }, "nametag": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.nametag }, "nametag_val": { "type": "texteditor", "bounds" : { "y": -1, "x" : 180, "w" : 370, }, "multiline" : false, "wordwrap" : false, "default" : settings.nametag_val, "visible": false, }, "spacer_bottom" : { "type" : "text", "label" : " ", "just" : "r", "bounds" : { "x": 0, "w" : 200, "h" : 20 }, }, // -------------------------------- "reset" : { "type" : "button", "label" : "reset", "bounds" : { "x": 340, "w" : 100 }, "returns" : -1 }, "save" : { "type" : "button", "label" : "save", "bounds" : { "y": -1, "x": 450, "w" : 100 }, "returns" : 1 }, // -------------------------------- }; // -------------------------------- return dialog(form, 'dialog_settings_callback'); // -------------------------------- } // =============================================================================