Help with assigning multiple outputs / notifier keepAlive and other...
Hey team,
I whipped this up last night so forgive any verbose or janky logic. I've been working on some prep scripts for my mixes. This script is basically designed to bounce all selected tracks into a stereo stem - my assistant and I do a lot of this stuff, bouncing down doubles, harmonies, BVs, etc all into a single stem.
While it might appear like there are a lot easier ways to do this, this is the most reliable way to avoid bouncing things through mix busses, trying to take up send slots (if all bases are loaded), removing existing assignments, avoids routing folders to maintain existing routing and more. I always need to leave breadcrumbs with files and tracks in the event things change or I want to redo things or recall things, but I also just hate having 40 live tracks of BVs even in a folder, etc.
The way it works:
Make selection of tracks you want to stem down
Checks for pans (I typically want to stem things down in full L-R fashion)
Name the bounce track as the prompt appears for new aux and routing assignment
Commits Aux track
Deletes Aux, replaces with bounced audio track
In the comments box add all the source track names that make up that stem
If all tracks that were previously assigned share the identical output assignment, reassign the output of the newly created track to that same output
Select source tracks, remove the additional output path we assigned so they're back to how they were originally
Put in a basic folder
Hide and make inactive
Anyway it works pretty well straight off the bat. There are just a few challenges i'm having and might need some soundflow schooling.
The first is the assignment menu selection aka:
// Assign outputs
sf.ui.proTools.selectedTrack.outputPathButton.popupMenuSelect({
isShift: true,
isOption: true,
isControl: true,
menuPath: ["new track..."]
});
When called with these keyboard modifiers I'm noticing it's reliable maybe 95-98% of the time. Granted if you skim the code you'll notice I call it I have a bunch of tracks selected, I'm assuming that might be where things are coming loose. The idea is it's meant to add an additional assignment to the output path of ALL selected tracks. However, I'm noticing in 2-5% of cases it's only ever doing it successfully to the first track as part of the selection.
I've tried invalidating a few ways, reselecting the tracks before calling the function and it still will still sometimes only assign the very first track. Next step is to incorporate some kind of check across all the tracks to ensure they've been successfully routed and if not do it track-by-track. While doing it this way from the get go would likely be more reliable, it'd defeat the purpose and ultimately make doing it manually quicker.
The other thing I could use schooling on is the soundflow progress notifier. Is there a way to keep it alive without setting an arbitrary time? (Currently 10mins). I tried -1 assuming that'd keep it alive but sadly that did not work. There's also the challenge of if the script fails halfway through the notifier stays alive unless it's manually dismissed. A small inconvenience, but I was wondering if there's a way for it to disappear in the event the script has ultimately failed.
function updateProgressNotification(notificationID) {
currentStepIndex = Math.min(currentStepIndex, steps.length - 1);
sf.interaction.notify({
uid: notificationID,
title: "Creating Track Stem",
progress: currentStepIndex / (steps.length - 1),
message: `${steps[currentStepIndex]}`,
keepAliveMs: 600000, // Keep alive for 10 minutes
});
}
I have a few other things I'm going to throw in there, but these are the two main things I'm having trouble with. Feel free to poke fun at other parts of the script if it's absurd!
Full Script:
const removeCm = true;
const newAffix = " ST"
const checkPanValues = true; // Check if all selected tracks are panned yet
let selectedTrackOutputsTheSame = false; // Initiate are output assignments the same boolean
let trackOutputAssignments = [];
// Initialize pan check
let panIsZero = false;
let tracksPannedCenter = [];
// Define all steps in the process
const steps = [
"Enter Stem Name",
"Creating track and routing assignments",
"Bouncing in progress - please wait",
"Renaming and updating track properties",
"Adding comments to track",
"Setting output routing",
"Cleaning up original source tracks",
"Process complete"
];
let currentStepIndex = 0;
function clearSolos() {
// Clear any existing solos
sf.ui.proTools.mainWindow.counterDisplay.mouseClickElement({
relativePosition: { x: 299, y: 67 }
});
}
function getInputPath() {
const inputPathButton = sf.ui.proTools.selectedTrack.inputPathButton;
const inputPath = inputPathButton.popupMenuFetchAllItems().menuItems.filter(mi => {
return mi.element.isMenuChecked;
})[0].path
sf.ui.proTools.appActivateMainWindow(); //Hack to close popup menu
return inputPath;
}
function getOutputPath() {
const outputPathButton = sf.ui.proTools.selectedTrack.outputPathButton;
const outputPath = outputPathButton.popupMenuFetchAllItems().menuItems.filter(mi => {
return mi.element.isMenuChecked;
})[0].path
sf.ui.proTools.appActivateMainWindow(); //Hack to close popup menu
return outputPath;
}
function addComments(selectedTrackNames) {
let comment = selectedTrackNames.join(", ");
const mainWindow = sf.ui.proTools.focusedWindow.title.value.match(/^(\w+): /)[1];
const viewCommentsMenuItem = sf.ui.proTools.getMenuItem('View', `${mainWindow} Window Views`, 'Comments');
const areCommentsVisible = viewCommentsMenuItem.isMenuChecked;
if (!areCommentsVisible) {
viewCommentsMenuItem.elementClick();
sf.ui.proTools.mainWindow.invalidate();
}
const tracksWithComments = sf.ui.proTools.selectedTrackHeaders;
try {
tracksWithComments.forEach((track, i, arr) => {
track.textFields.first.elementClick();
track.textFields.first.elementSetTextFieldWithAreaValue({ value: comment });
if (i === (arr.length - 1)) sf.keyboard.press({ keys: 'return' });
});
} finally {
if (!areCommentsVisible) viewCommentsMenuItem.elementClick();
}
}
function interogatePanValues() {
const selectedTracks = sf.app.proTools.tracks.invalidate().allItems.filter(t => t.isSelected).map(t => t.name);
selectedTracks.forEach(trackName => {
const track = sf.ui.proTools.trackGetByName({ name: trackName }).track;
// Check for tracks output
trackOutputAssignments.push(track.outputPathButton.value.invalidate().value)
// Check pan slider value
const panSlider = track.groups.whoseTitle.is("Audio IO").first.sliders.whoseTitle.contains(" Pan").first;
if (panSlider) {
const panTitle = panSlider.title;
if (panTitle.value.endsWith(">0<")) {
panIsZero = true;
tracksPannedCenter.push(trackName);
}
}
});
if (tracksPannedCenter.length > 0) {
sf.interaction.displayDialog({
buttons: ["Proceed Anyway", "Cancel"],
cancelButton: "Cancel",
defaultButton: "Cancel",
prompt: `Current selection includes ${tracksPannedCenter.length} tracks with pan values set to 0. Do you want to proceed?`,
title: "Pan Value Warning"
}).button;
}
}
function moveSourceTracksToFolder(bouncedTrack) {
sf.ui.proTools.menuClick({
menuPath: ["Track", "Move to New Folder..."],
});
// New track dialog
const newTracksDlg = sf.ui.proTools.windows.whoseTitle.is("Move To New Folder").first;
newTracksDlg.elementWaitFor();
if (newTracksDlg.popupButtons.first.value.invalidate().value !== "Basic Folder") {
newTracksDlg.popupButtons.first.popupMenuSelect({
menuPath: ["Basic Folder"]
});
}
newTracksDlg.textFields.whoseTitle.is("Track name").first.elementSetTextFieldWithAreaValue({ value: " SRC TRKS - " + bouncedTrack });
newTracksDlg.buttons.whoseTitle.is("Create").first.elementClick();
newTracksDlg.elementWaitFor({ waitType: "Disappear" });
sf.ui.proTools.invalidate();
const trackFolder = sf.app.proTools.tracks.invalidate().allItems.filter(t => t.type === "BasicFolder").map(t => t.name);
sf.app.proTools.setTrackOpenState({ trackNames: trackFolder, enabled: false });
}
function commitTracks() {
sf.ui.proTools.menuClick({
menuPath: ["Track", "Commit..."],
});
// New track dialog
const commitDlg = sf.ui.proTools.windows.whoseTitle.is("Commit Tracks").first
commitDlg.elementWaitFor();
commitDlg.checkBoxes.whoseTitle.is("Consolidate Clips").first.checkboxSet({ targetValue: "Enable" });
commitDlg.checkBoxes.whoseTitle.is("Volume and Mute").first.checkboxSet({ targetValue: "Disable" });
commitDlg.checkBoxes.whoseTitle.is("Pan").first.checkboxSet({ targetValue: "Disable" });
commitDlg.checkBoxes.whoseTitle.is("Sends").first.checkboxSet({ targetValue: "Disable" });
commitDlg.checkBoxes.whoseTitle.is("Group Assignments").first.checkboxSet({ targetValue: "Disable" });
commitDlg.checkBoxes.whoseTitle.is("Elastic Audio algorithm").first.checkboxSet({ targetValue: "Enable" });
commitDlg.checkBoxes.whoseTitle.is("ARA plugin assignment").first.checkboxSet({ targetValue: "Disable" });
commitDlg.checkBoxes.whoseTitle.is("Offline").first.checkboxSet({ targetValue: "Enable" });
commitDlg.popupButtons.allItems[1].popupMenuSelect({
menuPath: ["Delete"],
});
commitDlg.buttons.whoseTitle.is("OK").first.elementClick();
commitDlg.elementWaitFor({ waitType: "Disappear" });
}
function createNewTrack() {
// Assign outputs
sf.ui.proTools.selectedTrack.outputPathButton.popupMenuSelect({
isShift: true,
isOption: true,
isControl: true,
menuPath: ["new track..."]
});
// Necessary hack for popup menu
sf.ui.proTools.appActivateMainWindow();
// New track dialog
const newTracksDlg = sf.ui.proTools.windows.whoseTitle.is("New Track").first;
newTracksDlg.elementWaitFor();
newTracksDlg.checkBoxes.whoseTitle.is("Create next to current track").first.checkboxSet({
targetValue: "Disable",
});
if (newTracksDlg.popupButtons.allItems[2].value.invalidate().value !== "Stereo") {
newTracksDlg.popupButtons.allItems[2].popupMenuSelect({
menuPath: ['Stereo'],
});
}
if (newTracksDlg.popupButtons.first.value.invalidate().value !== "Aux Input") {
newTracksDlg.popupButtons.first.popupMenuSelect({
menuPath: ['Aux Input']
});
}
// Wait for the "Open" window to disappear
while (newTracksDlg.invalidate().exists) {
sf.wait({ intervalMs: 300 });
}
newTracksDlg.elementWaitFor({ waitType: "Disappear", });
}
function waitForBounceToComplete() {
//Wait for bounce to finish (Wait for progress dialog to disappear)
sf.wait({ intervalMs: 10000 });
try {
//Wait for bounce to finish (Wait for progress dialog to disappear)
sf.ui.proTools.confirmationDialog.elementWaitFor({ waitType: 'Disappear', timeout: -1 }); //-1 is endless timeout (cancel by Ctrl+Shift+Esc)
} catch (e) {
// Handle exception
}
while (sf.ui.frontmostApp.title.value !== "Pro Tools") {
sf.wait({ intervalMs: 500 });
}
}
function areAllOutputAssignmentsIdentical(array) {
// Return true for empty arrays or arrays with only one element
if (array.length <= 1) return true;
// Get the first item to compare against
const firstItem = array[0];
// Check if all items match the first item
return array.every(item => item === firstItem);
}
function updateProgressNotification(notificationID) {
currentStepIndex = Math.min(currentStepIndex, steps.length - 1);
sf.interaction.notify({
uid: notificationID,
title: "Creating Track Stem",
progress: currentStepIndex / (steps.length - 1),
message: `${steps[currentStepIndex]}`,
keepAliveMs: 600000, // Keep alive for 10 minutes
});
}
function main() {
// Create a unique notification ID for this script execution
const notificationID = "stem-create-" + Date.now();
sf.ui.proTools.appActivateMainWindow();
sf.ui.proTools.mainWindow.invalidate();
if (sf.ui.proTools.selectedTrackNames.length < 1) {
alert("Please select some tracks and try again");
throw 0;
}
// Check for selection
if (sf.ui.proTools.selectedTrackNames.length === 1) {
sf.interaction.displayDialog({
buttons: ["Proceed Anyway", "Cancel"],
cancelButton: "Cancel",
defaultButton: "Cancel",
prompt: `You've only selected one track for bouncing, are you sure you want to proceed?`,
title: "Warning"
}).button;
}
sf.ui.proTools.menuClick({
menuPath: ["View", "Edit Window Views", "I/O"],
targetValue: "Enable",
});
// Get selected track Names
const selectedSourceTracks = sf.ui.proTools.selectedTrackNames;
// Select first track and ensure it is visible / scrolled in Edit window
sf.ui.proTools.trackSelectByName({ names: [selectedSourceTracks[0]] });
sf.ui.proTools.selectedTrack.trackScrollToView();
// Get the output path of the first track
const outputPath = getOutputPath();
// Reselect originally selected tracks
sf.ui.proTools.trackSelectByName({ names: selectedSourceTracks });
if (checkPanValues) {
interogatePanValues();
}
// Step: Create New Track
updateProgressNotification(notificationID);
sf.ui.proTools.mainWindow.invalidate();
createNewTrack();
sf.ui.proTools.mainWindow.invalidate();
// Step: Create track and assign routing
currentStepIndex++;
updateProgressNotification(notificationID);
// Let's just select the new audio track created
const tracks = sf.app.proTools.tracks.invalidate().allItems.filter(t => t.isSelected).map(t => t.name);
const bouncedTrack = tracks[tracks.length - 1];
// Select just the newly created track
sf.app.proTools.selectTracksByName({ trackNames: [bouncedTrack], selectionMode: "Replace" });
sf.app.proTools.setTrackSoloState({ trackNames: selectedSourceTracks, enabled: true });
// Get the input path assignment - we'll need this later to unassign from the source tracks
const inputPath = getInputPath();
// Commit the AUX
commitTracks();
// Step: Bouncing in progress
currentStepIndex++;
updateProgressNotification(notificationID);
// Wait for bounce to complete
waitForBounceToComplete();
clearSolos();
// Step: Rename and update track properties
currentStepIndex++;
updateProgressNotification(notificationID);
// Remove .cm and update name with new affix
if (removeCm) {
sf.app.proTools.renameTrack({
oldName: sf.ui.proTools.selectedTrackNames[0].slice(0, -3),
newName: sf.ui.proTools.selectedTrackNames[0] + newAffix,
});
}
// Disable the solo safe state
sf.app.proTools.setTrackSoloSafeState({ trackNames: sf.ui.proTools.selectedTrackNames, enabled: false });
// Step: Add comments to track
currentStepIndex++;
updateProgressNotification(notificationID);
// Add comments to display source tracks
addComments(selectedSourceTracks);
// Step: Set output routing
currentStepIndex++;
updateProgressNotification(notificationID);
// Check if all track outputs are the same by comparing the array
selectedTrackOutputsTheSame = areAllOutputAssignmentsIdentical(trackOutputAssignments);
// Assign new track to original output if it passed the vibe check
if (selectedTrackOutputsTheSame) {
sf.ui.proTools.selectedTrack.outputPathButton.popupMenuSelect({
menuPath: outputPath
});
sf.ui.proTools.appActivateMainWindow(); // Hack to close menu
}
// Step: Clean up original source tracks
currentStepIndex++;
updateProgressNotification(notificationID);
// Reselect original source tracks
sf.app.proTools.selectTracksByName({ trackNames: selectedSourceTracks, selectionMode: "Replace" });
// Remove the bounce path from source tracks
sf.ui.proTools.selectedTrack.outputPathButton.popupMenuSelect({
isShift: true,
isOption: true,
isControl: true,
menuPath: inputPath
});
// Needed to ensure path deselect above
sf.ui.proTools.appActivateMainWindow();
// Move source tracks to a folder and make inactive for archival purposes
moveSourceTracksToFolder(bouncedTrack);
// Hide source tracks and new folder
sf.ui.proTools.trackHideAndMakeInactiveSelected();
// Final step: Process complete
currentStepIndex++;
updateProgressNotification(notificationID);
// Final notification that auto-dismisses
sf.interaction.notify({
uid: notificationID,
title: "Creating Track Stem",
progress: 1,
message: "Process complete!",
keepAliveMs: 1500, // Auto-dismiss after 1.5 seconds
});
}
main();
Thanks!
- Chris Shaw @Chris_Shaw2025-04-12 17:42:19.299Z
You might need to wait for all of the output values to change before moving on.
Something like this should work the relevant parts are line 4 thru 21:// Get selected track Names const selectedSourceTracks = sf.ui.proTools.selectedTrackNames; const lastSelectedTrackName = selectedSourceTracks.slice(-1)[0] function getOutputPathBtnValue(trackName) { const trackHeader = sf.ui.proTools.trackGetByName({ name: trackName }).track; const outputPathButton = trackHeader.outputPathButton; return outputPathButton.value.invalidate().value } const origLastSelectedTrackOutputValue = getOutputPathBtnValue(lastSelectedTrackName) // Assign outputs routine here <----- // wait for last selected track's output assign to change sf.waitFor({ callback: () => origLastSelectedTrackOutputValue !== getOutputPathBtnValue(lastSelectedTrackName) }) // Continue on
This waits for the value of the last selected track's output assign button value to change before continuing on
- In reply toTristan⬆:Chris Shaw @Chris_Shaw2025-04-12 17:58:55.743Z2025-04-12 18:57:41.338Z
For the notification issue, move
const notificationID = "stem-create-" + Date.now();
out of the main function then add anotificationID
argument when defining the main function -
function main(notificationID){…}
.
Then wrap the whole thing in a try catch block:const notificationID = "stem-create-" + Date.now(); try { main(notificationID) } catch (err) { sf.interaction.notify({ uid: notificationID, title: "Creating Track Stem", message: `Failed because: ${err}`, keepAliveMs: 2000, }); }
Or wrap the above try/catch block code around lines 250-400
Chris Shaw @Chris_Shaw2025-04-12 18:00:21.805Z
As far a as I know the only way to keep a notification alive indefinitely is to assign a very long
keepAlive
value. A value of -1 dismisses it (I believe)- In reply toChris_Shaw⬆:TTristan Hoogland @Tristan
This is great btw Chris - awesome!
- In reply toTristan⬆:Chris Shaw @Chris_Shaw2025-04-12 18:22:14.422Z2025-04-12 18:51:24.262Z
I refactored
createNewTrack()
with the above code.
It seems to be missing as step as it doesn't click the 'Create' button in the New Track Dialog.
I included a couple of extra invalidations.
I haven't tested this in context of the whole script so let me know if it works.function createNewTrack() { function getOutputPathBtnValue(trackName) { sf.ui.proTools.mainWindow.invalidate(); const trackHeader = sf.ui.proTools.trackGetByName({ name: trackName }).track; const outputPathButton = trackHeader.outputPathButton; return outputPathButton.value.invalidate().value } sf.ui.proTools.mainWindow.invalidate(); const origSelectedTracks = sf.ui.proTools.selectedTrackNames const lastSelectedTrackName = origSelectedTracks.slice(-1)[0] const origLastSelectedTrackOutputValue = getOutputPathBtnValue(lastSelectedTrackName) // Assign outputs by creating a new track sf.ui.proTools.selectedTrack.outputPathButton.popupMenuSelect({ isShift: true, isOption: true, isControl: true, menuPath: ["new track..."], }); // Necessary hack for popup menu sf.ui.proTools.appActivateMainWindow(); // New track dialog const newTracksDlg = sf.ui.proTools.windows.whoseTitle.is("New Track").first; newTracksDlg.elementWaitFor(); newTracksDlg.checkBoxes.whoseTitle.is("Create next to current track").first.checkboxSet({ targetValue: "Disable", }); if (newTracksDlg.popupButtons.allItems[2].value.invalidate().value !== "Stereo") { newTracksDlg.popupButtons.allItems[2].popupMenuSelect({ menuPath: ['Stereo'], }); } if (newTracksDlg.popupButtons.first.value.invalidate().value !== "Aux Input") { newTracksDlg.popupButtons.first.popupMenuSelect({ menuPath: ['Aux Input'] }); } // Click 'Create' newTracksDlg.buttons.whoseTitle.is("Create").first.elementClick() // Wait for the "New Tracks" window to disappear while (newTracksDlg.invalidate().exists) { sf.wait({ intervalMs: 300 }); } newTracksDlg.elementWaitFor({ waitType: "Disappear", }); // Wait for last selected track's output assign button value to change sf.waitFor({ callback: () => origLastSelectedTrackOutputValue !== getOutputPathBtnValue(lastSelectedTrackName), }, 'Error: Output of last originally selected track has not changed') }
- TTristan Hoogland @Tristan
Appreciate you chiming in and making some changes here, Chris! I really need to start implementing callbacks as standard, I've only just started going through and updating all my previous scripts with them. This is working really well so far, will do some more stress testing over the next few days while I clean it up. Thanks so much man! Always learning something new from you.