Hey @Chad !
I will send a video via email because I don't want the specifics of the content to be super public and it might be somewhat difficult to explain otherwise. I'm looking for a script that will automate exporting commercial audio spots. There are quite a few that need to be named and bounced, and the length is dictated by a clipgroup on a track called length, and the name is dictated by a marker at the head of said clip group. Basically the script would bounce every clip group denoted region using the accompanying marker as the naming guide. Do you think this is possible? Or would it work better if the name was on the clip group itself?
- Chad Wahlbrink @Chad2023-11-16 23:04:42.361Z
Hey @Dane_Butler!
Thank you for making a forum post. If you can, can you give a bit more detail of the steps you are looking to automate?
From what I can see here, it seems like you'd like to:- Set a timeline selection based on a clip group
- Capture the name of a Memory Location on the global ruler
- Click "File > Bounce Mix..."
- Ensure "Add Mp3" is selected
- Click "Bounce"
- Click "OK" on the MP3 dialog.
- Wait for the bounce to complete
- Repeat for each selected clip
I believe each of those steps should be automatable. I'll see if I can work up a script in the next few days.
- DDane Butler @Dane_Butler
Yes, I believe you nailed it! I have lots of regions to bounce, and each region is its own self contained mix!
The ability to automate the renaming and exporting process would be extremely helpful for these radio ads!
- In reply toDane_Butler⬆:Chad Wahlbrink @Chad2023-11-17 00:03:27.462Z
@Dane_Butler, give this a shot. This script should perform the steps outlined above. It is good practice to do a "test bounce" and make sure all the bounce settings are set up how you want them for the WAV/MP3 before bouncing. Then, select all of the clip groups on the "length" track and run the script.
Note: This script doesn't currently overwrite files if they exist. So it's best to delete files in the "bounced files" folder if you are "re-printing."
if (!sf.ui.proTools.isRunning) throw `Pro Tools is not running`; sf.ui.proTools.appActivateMainWindow(); sf.ui.proTools.mainWindow.invalidate(); let memLocs; // Convert Pro Tools Version to Number const ptVersion = sf.ui.proTools.appVersion.split('.').map(Number).slice(0, 2).reduce((p, c, i) => p + Math.pow(100, 2 - i) * c, 0); function getMemoryLocations() { // 2023.03 to 2023.06 support const tableIndex = ptVersion >= 230600 ? 1 : 0; const memLocTableNameField = ptVersion >= 230600 ? 2 : 1; const memLocTableTimeCodeField = ptVersion >= 230600 ? 4 : 2; sf.ui.proTools.appActivate(); sf.ui.proTools.mainWindow.elementRaise(); var i = 0; // Open the Memory Locations Window while (i++ < 5) { if (!sf.ui.proTools.memoryLocationsWindow.invalidate().exists) { sf.ui.proTools.getMenuItem('Window', 'Memory Locations').elementClick({}, 'Could not click to open Memory Locations window'); sf.ui.proTools.memoryLocationsWindow.elementWaitFor(); } // Define the memory locations table var table = sf.ui.proTools.memoryLocationsWindow.invalidate().tables[tableIndex].children.whoseRole.is("AXRow"); if (!table.exists) { sf.ui.proTools.memoryLocationsWindow.windowClose(); } } if (!table.exists) throw 'Could not find memory locations table'; // define the memory locations items var items = []; table.forEach(function (item, i) { var memLocNum = item.children.whoseRole.is("AXCell").allItems[1].children.whoseRole.is("AXStaticText").first.title.value.replace("Selected. ", ""); var memLocName = item.children[memLocTableNameField].children.whoseRole.is("AXStaticText").first.title.value.replace("Selected. ", ""); var timeStart = item.children[memLocTableTimeCodeField].children.whoseRole.is("AXStaticText").first.title.value.replace("Selected. ", ""); if (memLocNum === null || memLocName === null) return; items.push({ memLocNum, memLocName, timeStart }); }); return items; } function bounceMix(bounceName) { // Click "Bounce Mix..." from File Menu sf.ui.proTools.menuClick({ menuPath: ["File", "Bounce Mix..."], }, `Could not click "Bounce Mix" in Pro Tools`); // Define Bounce Window let bounceWin = sf.ui.proTools.windows.whoseTitle.is("Bounce Mix").first; // Wait for Bounce Window to Appear bounceWin.elementWaitFor({ waitType: "Appear", }); // Set Track Name sf.ui.proTools.windows.whoseTitle.is("Bounce Mix").first.textFields.first.elementSetTextFieldWithAreaValue({ value: bounceName }); // Check the MP3 checkbox if (!sf.ui.proTools.windows.whoseTitle.is("Bounce Mix").first.groups.whoseTitle.is("Audio").first.checkBoxes.whoseTitle.is("Add MP3").first.isCheckBoxChecked) { sf.ui.proTools.windows.whoseTitle.is("Bounce Mix").first.groups.whoseTitle.is("Audio").first.checkBoxes.whoseTitle.is("Add MP3").first.checkboxSet({ targetValue: "Enable" }) } // Click Bounce bounceWin.buttons.whoseTitle.is("Bounce").first.elementClick(); // Wait for the MP3 Dialog sf.ui.proTools.windows.whoseTitle.is("MP3").first.elementWaitFor({ waitType: "Appear" }); // Click "OK" sf.ui.proTools.windows.whoseTitle.is("MP3").first.buttons.whoseTitle.is("OK").first.elementClick(); // Wait for the MP3 Dialog to Disappear sf.ui.proTools.windows.whoseTitle.is("MP3").first.elementWaitFor({ waitType: "Disappear" }); // Wait for Bounce Window to Disappear bounceWin.elementWaitFor({ waitType: "Disappear", timeout: -1, onError: "Continue" }); // Wait for Bounce to finish (Wait for bounce dialog to appear, then to disappear) var bouncingLabel = sf.ui.proTools.windows.whoseTitle.is('').first.children.whoseRole.is("AXStaticText").whoseValue.is('Bouncing...').first; bouncingLabel.elementWaitFor({ timeout: 1000, onError: "Continue" }); bouncingLabel.elementWaitFor({ waitType: 'Disappear', timeout: -1 }); // -1 is endless timeout (cancel by Ctrl+Shift+Esc) } function bounceEachClip() { // Get the current timecode let currentTimeCode = sf.ui.proTools.invalidate().mainWindow.counterDisplay.textFields.whoseTitle.is("Edit Selection Start").first.value.value; // find the memory location with the current timecode let currentMemoryLocationName = memLocs.filter(x => x.timeStart === currentTimeCode)[0].memLocName; // bounce the mix bounceMix(currentMemoryLocationName); } function main() { // Set Main Counter to Samples if (!sf.ui.proTools.getMenuItem('View', 'Main Counter', 'Samples').isMenuChecked) { sf.ui.proTools.menuClick({ menuPath: ['View', 'Main Counter', 'Samples'], }); } try { // Get the memory locations memLocs = getMemoryLocations(); // bounce each selected clip sf.ui.proTools.clipDoForEachClip({ action: bounceEachClip }) } finally { // Set Main Counter to Timecode if (!sf.ui.proTools.getMenuItem('View', 'Main Counter', 'Timecode').isMenuChecked) { sf.ui.proTools.menuClick({ menuPath: ['View', 'Main Counter', 'Timecode'], }); } } } main(); log('finished bouncing');
- DDane Butler @Dane_Butler
Thanks chad! Impressive stuff as per usual. I'll give this a go when I'm back in front of my main rig.
Btw, for my edification, what does this line do?
const ptVersion = sf.ui.proTools.appVersion.split('.').map(Number).slice(0, 2).reduce((p, c, i) => p + Math.pow(100, 2 - i) * c, 0);
I see it uses some array methods but the .reduce((p,c,i)=> p+ Math.pow(100,2-i)*c,0);
...is rather confounding!
Chad Wahlbrink @Chad2023-11-17 00:22:25.231Z
Honestly, I haven't taken the time to unpack that function either! That little bit of magic was passed onto me by Christian. The reduce method is a fairly complex array method. I'd recommend checking out this video if interested:
https://youtu.be/6_XzV25rkcE?si=OtcnC10lhYs5Jwh8
The result is a number version of the current pro tools version. Avid changed up some of the GUI for memory locations starting in Pro Tools 2023.06 when they introduced Track Markers. I am using that to differentiate how the Memory Locations window is read based on the Pro Tools version since I wasn't 100% sure what version you are using.- DDane Butler @Dane_Butler
Ok I just checked it out and it's working! Man I can't thank you enough. These two scripts alone have seriously improved my quality of life. Most appreciated. I'm gonna check out that youtube video now!
- DDane Butler @Dane_Butler
Heeeey @Chad ! I have returned with hopefully the last request for a while. I'm back on another video game, but this time on the voice design side. What I was wondering is....
would you be able to create a macro that does something very similar to the last one, but with one key difference! Rather than exporting and copying marker data, the operation would be ....
- Select all clips on a timeline (Clip groups)
- Select each clip individually, and then once a clip is selected...
- Press record until the recording reaches the end of the selected clip (Leaving you with two clips of equal size, the one selected, and the one on the track that was record enabled.
- Go to the next clip and rinse and repeat until all clips have been printed.
Basically I have an effect chain that I have to run each individual clip through onto a print track.
**Here's where it gets juicy. **
Some of the clips I'm printing (depending on the thing I'm designing) will have reverb on them.
Would there be a way to have an optional modal that pops up and asks me how much of a "tail" I would like to add on to the end of the file? So if say it's printing and moving between clips that are each 3 seconds long, the modal user prompt would factor in a 1 second tail to be automatically added to the print sequence per clip (for a total length of 4 seconds)? I can send a video offline if need be!
Chad Wahlbrink @Chad2023-12-01 16:34:37.803Z
Hey @Dane_Butler, I'll take a look at this workflow next week. Thanks!
- DDane Butler @Dane_Butler
hey Chad! Just wanted to check in on you! Any progress? :) I super appreciate it again.
- In reply toDane_Butler⬆:
Chad Wahlbrink @Chad2023-12-08 15:49:27.655Z
Hey @Dane_Butler,
Here's a solution that is working on my rig currently. It does use the Pro Tools SDK, so if you are on a version of Pro Tools before 2023.6 or so, then I'll need to adjust that aspect to work solely on UI.
Here's a little video walking you through how it works:
https://www.dropbox.com/s/l858ivjy5c47g0p/2023-12-08 Render Voice Sound Design.mp4?dl=0if (!sf.ui.proTools.isRunning) throw `Pro Tools is not running`; sf.ui.proTools.appActivateMainWindow(); sf.ui.proTools.mainWindow.invalidate(); function recordClipToNewTrack() { if (offset != '') { // SDK - Get Sample Rate let sR = sf.app.proTools.getSessionSampleRate().sampleRate.split('SR')[1]; //SDK - Capture Starting Selection let startSelection = sf.app.proTools.getTimelineSelection({ timeScale: "Samples" }); //Add Offset to Selection Out Time let newStop = Number(startSelection.outTime) + (Number(offset) * Number(sR)); // SDK - Set Timeline Selection with Offset sf.app.proTools.setTimelineSelection({ inTime: startSelection.inTime, outTime: String(newStop), }); } //Record Enable sf.ui.proTools.mainWindow.transportViewCluster.transportButtons.recordEnableButton.elementClick(); //Play to start record sf.ui.proTools.mainWindow.transportViewCluster.transportButtons.playButton.elementClick(); // Wait for record to finish while (sf.ui.proTools.isPlaying) { sf.wait({ intervalMs: 200 }); } } // Using this function to commit fades before printing to better work with doForEachClip() function renderFades() { // Open Gain Audio Suite let win = sf.ui.proTools.audioSuiteOpenPlugin({ category: 'Other', name: 'Gain', }, `Could not find AudioSuite plugin in menu "AudioSuite" -> "Other" -> "Gain"`).window; // Set Gain Audio Suite Settings win.audioSuiteSetOptions({ processingInputMode: "EntireSelection", processingOutputMode: "CreateIndividualFiles", }); // Render Gain Audio Suite win.audioSuiteRender(); // Close Gain Audio Suite win.windowClose(); } function main() { // Ensure Transport sf.ui.proTools.transportEnsureCluster(); // Check for record enabled track if (!sf.ui.proTools.mainWindow.transportViewCluster.groups.whoseTitle.is("Normal Transport Buttons").first.buttons.whoseTitle.is("Track Record Enabled").exists) { alert(`Please Record Enable a Destination Track`); throw 0; } // Select all Clips sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Select All'], }); renderFades(); try { // Record Each Selected Clip sf.ui.proTools.clipDoForEachClip({ action: recordClipToNewTrack }) } finally { // Set Main Counter to Timecode if (!sf.ui.proTools.getMenuItem('View', 'Main Counter', 'Timecode').isMenuChecked) { sf.ui.proTools.menuClick({ menuPath: ['View', 'Main Counter', 'Timecode'], }); } } } // Would you like to print extra for reverb? let offset = prompt(`Would You like To add extra time (in seconds)?`); main();
- DDane Butler @Dane_Butler
Legend! I'll fire this up today and see where it gets me! Thank you again, if this works it will save my can on this second batch of DZN characters.
- DDane Butler @Dane_Butler
The script worked awesome! The only thing that threw an error was if I tried to enter a time in seconds into the window! I manually drug the flag to get the post roll to change which was a good workaround. This is very exciting though! Huge leap forward in workflow. I have so many files to just "stamp" with an effect I designed last batch so this is great!
Chad Wahlbrink @Chad2023-12-21 18:28:35.212Z
I'm glad it worked for ya!
Happy Holidays!
- DDane Butler @Dane_Butler
Happy holidays!
- In reply toChad⬆:GGeorge Hinson @George_Hinson
Hey @Chad thanks for this! I'm getting issues on line 100
'TypeError: Cannot read property 'memLocName' of undefined'I wondered if I am selecting something incorrectly? Very new to this any advice would be great,
Thanks,
All the best,George