Here's a script I built based on a discussion here:
Remove all inactive plugins/sends
I made a few changes to this.
- It passes all the tracks per inactive insert to an array and then does the removal in one go. This is faster than removing for each track.
- if there is no track selected it asks to proceed for all the tracks.
- There are some checks to compensate for unusual track heights or positions where it couldnt access the insert element.
- I've slightly optimised the code as well.
- It asks for a choice of all Plugins and Inserts or just Plugins or just inserts. For a more configurable one, check out the one I mention below. That code is open as well in the package in case you want to use it!
There is a Soundflow Template version of this in my Sree - Pro Tools Editing Package where you can build your own presets and choose which inserts and / or sends need to be checked for inactive Plugins or sends or both. Hope it helps!
// Closes Pro Tools confirmation dialogs if they contain "Remove" or "Change" buttons
function closeDialog() {
['Remove', 'Change'].forEach(title => {
const button = sf.ui.proTools.confirmationDialog.invalidate().buttons.whoseTitle.is(title).first;
if (button.exists) {
button.elementClick();
sf.ui.proTools.confirmationDialog.elementWaitFor({ waitType: "Disappear" });
}
});
}
// Finds inactive inserts and sends in the provided tracks and maps them
function findInactiveItems(tracks) {
let map = { inserts: {}, sends: {} };
let elements = { inserts: [], sends: [] };
let notificationID = sf.system.newGuid().guid;
tracks.forEach((track, index) => {
// Notify progress of processing tracks
sf.interaction.notify({
uid: notificationID,
title: "Processing Automation Data",
progress: (index + 1) / tracks.length,
message: `Analysing Session: ${index + 1} of ${tracks.length} tracks`
});
if (track) {
// Process insert buttons
track.insertSelectorButtons.forEach((button, i) => {
if (button.exists && button.invalidate().value.value.startsWith("inactive")) {
const slot = i + 1;
if (!map.inserts[slot]) {
map.inserts[slot] = [];
elements.inserts.push(slot);
}
map.inserts[slot].push(track.normalizedTrackName);
}
});
// Process send buttons
track.sendSelectorButtons.forEach((button, i) => {
if (button.exists && button.invalidate().value.value.startsWith("inactive")) {
const slot = i + 1;
if (!map.sends[slot]) {
map.sends[slot] = [];
elements.sends.push(slot);
}
map.sends[slot].push(track.normalizedTrackName);
}
});
}
});
return { map, elements };
}
// Removes items (inserts or sends) from the specified tracks and slots
function removeItems(names, type, slot) {
sf.keyboard.press({ keys: "home" });
sf.ui.proTools.trackSelectByName({ names, deselectOthers: true });
const original = sf.ui.proTools.selectedTrackNames;
if (original.length > 0) {
sf.ui.proTools.trackGetByName({ name: original[0] }).track.trackScrollToView();
}
sf.ui.proTools.trackSelectByName({ names, deselectOthers: true });
sf.ui.proTools.selectedTrack.trackInsertOrSendSelect({
insertOrSend: type,
pluginNumber: parseInt(slot, 10),
selectForAllSelectedTracks: true,
pluginPath: type === 'Insert' ? ['no insert'] : ['no send']
});
closeDialog();
sf.wait({ intervalMs: 250 });
}
// Batch removes inactive inserts or sends based on the selected option
function batchRemove(map, elements, option) {
let processedItems = 0;
let notificationID = sf.system.newGuid().guid;
const totalItems = elements.inserts.length + elements.sends.length;
const removeFromSlot = (slot, type, map) => {
removeItems(map[slot], type, slot);
// Notify progress of removal
sf.interaction.notify({
uid: notificationID,
title: "Removing Inactive Items",
progress: (++processedItems) / totalItems,
message: `Removing ${type}s: ${processedItems} of ${totalItems}`
});
};
if (option === "both" || option === "plugins") {
elements.inserts.forEach(slot => removeFromSlot(slot, 'Insert', map.inserts));
}
if (option === "both" || option === "sends") {
elements.sends.forEach(slot => removeFromSlot(slot, 'Send', map.sends));
}
sf.interaction.notify({
uid: notificationID,
title: "Removing Inactive Items",
progress: 100,
message: `Removal complete.`
});
}
// Sets the track size for the selected track
function setTrackSize(size) {
const selectedTrackH = sf.ui.proTools.selectedTrackHeaders[0];
try {
// Try setting track size using the right popup menu
selectedTrackH.popupMenuSelect({
anchor: "MidRight",
relativePosition: { x: -10, y: 0 },
menuPath: [size],
isOption: true,
onError: "Continue"
});
} catch (err) {
// If the right popup menu fails, try the left popup menu
if (selectedTrackH.frame.h <= 79) {
sf.ui.proTools.selectedTrack.popupButtons.allItems[2].popupMenuSelect({
menuPath: ["Track Height", size],
isOption: true
});
}
}
}
// Activates the specified Pro Tools menu options
function activateProToolsMenus(menuOptions) {
menuOptions.forEach(menuOption =>
sf.ui.proTools.menuClick({
menuPath: ["View", "Edit Window Views", menuOption],
targetValue: "Enable"
})
);
}
// Define Pro Tools menu options to be activated
const proToolsMenuOptions = ["Inserts A-E", "Inserts F-J", "Sends A-E", "Sends F-J"];
// Main script function that initiates the process based on user selection
function startScript() {
// User selection prompt
const userSelection = sf.interaction.selectFromList({
items: ["Remove all Inactive Plugins and Sends", "Remove all Inactive Plugins", "Remove all Inactive Sends"],
allowMultipleSelections: false,
prompt: "Choose what you want removed",
title: "Inactive Cleaner"
}).list;
if (userSelection.length === 0) return alert("No option selected. Exiting script.");
let chosenOption = userSelection[0];
sf.ui.proTools.appActivate();
sf.ui.proTools.mainWindow.invalidate();
const wasLinked = sf.ui.proTools.getMenuItem('Options', 'Link Track and Edit Selection').isMenuChecked;
if (wasLinked) sf.ui.proTools.menuClick({ menuPath: ["Options", "Link Track and Edit Selection"] });
if (!sf.ui.proTools.mainWindow.title.invalidate().value.startsWith("Edit: ")) {
sf.ui.proTools.menuClick({ menuPath: ["Window", "Edit"] });
}
activateProToolsMenus(proToolsMenuOptions);
sf.ui.proTools.invalidate();
let tracksForProcessing = sf.ui.proTools.selectedTrackHeaders;
let firstSelection = sf.ui.proTools.selectedTrackNames;
if (tracksForProcessing.length !== 0) {
sf.ui.proTools.trackGetByName({ name: firstSelection[0] }).track.trackScrollToView();
sf.ui.proTools.trackSelectByName({ names: firstSelection });
setTrackSize('small');
} else {
if (!confirm("No tracks are selected. Would you like to proceed with all visible active tracks?")) {
return alert("Operation cancelled.");
}
firstSelection = sf.ui.proTools.visibleTrackNames;
sf.ui.proTools.trackGetByName({ name: firstSelection[0] }).track.trackScrollToView();
const activeTrackNames = sf.app.proTools.tracks.invalidate().allItems.reduce((activeTrackNames, track) => {
if (!track.isInactive) activeTrackNames.push(track.name);
return activeTrackNames;
}, []);
sf.app.proTools.selectTracksByName({ trackNames: activeTrackNames });
setTrackSize('small');
tracksForProcessing = sf.ui.proTools.selectedTrackHeaders;
}
const { map, elements } = findInactiveItems(tracksForProcessing);
if (elements.inserts.length === 0 && elements.sends.length === 0) {
return alert("No inactive plugins or sends found.");
}
let option = chosenOption === "Remove all Inactive Plugins and Sends" ? "both" :
chosenOption === "Remove all Inactive Plugins" ? (elements.inserts.length === 0 ? (alert("No inactive plugins found."), undefined) : "plugins") :
(elements.sends.length === 0 ? (alert("No inactive sends found."), undefined) : "sends");
if (option) batchRemove(map, elements, option);
if (wasLinked) sf.ui.proTools.menuClick({ menuPath: ["Options", "Link Track and Edit Selection"] });
sf.ui.proTools.trackSelectByName({ names: firstSelection, deselectOthers: true });
alert("Operation completed.");
}
// Start the script
startScript();
- Ddanielkassulke @danielkassulke
Hi @Sreejesh_Nair . Very excited to use this - I think the way you've described it working is a really big improvement on previous attempts at scripts like this. I can't get this to work on either my macbook pro or my studio computer (both ventura, 2023.12). I've tried it on new sessions with just inactive sends or inserts or both, and existing, busier sessions with sends, inserts and both. It seems do everything except remove the inserts. As you can see from the screen recording I took, there's nothing obstructing the send/insert buttons, and you've obviously incorporated track height handling. I see that you posted this script a couple of days before 2023.12 was released, and I'm wondering if that ProTools update has made this fail? For context, even though in the video I chose to remove inactive sends and inserts, I got the same result with choosing sends or inserts by themselves. The "Analysing session for inactive plugins/sends. Please wait" and "Analysis complete. Proceeding." messages still get logged. Any ideas what might be causing this?
Thanks!
- SSreejesh Nair @Sreejesh_Nair
Thank you for pointing that out. I had forgotten to add the word "all" in the chosenOption. It should be fixed now! And while at it, I also added a check to ignore all inactive tracks. This was an issue if it was part of the selection.
- Ddanielkassulke @danielkassulke
Really amazing script. I am experiencing it working its magic now! A couple of other things I noticed: I assume
if (wasLinked)
should beif (!wasLinked)
? The other thing that I think worth incorporating is some scroll to view functionality so that the insert/send buttons can be accessed if the first selected track isn't visible.- SSreejesh Nair @Sreejesh_Nair
Hi.. Thank you.
The
wasLinked
logic may look confusing at first sight. The firstif (wasLinked)
turns the link edit off if true. At the end of the script, it doesn’t check for a new value. It references the first value. And turns it back on if it was on before the script ran (it’s a toggle). So the logic is if it was on before the functions were called, it would have been turned off. So on the completion of the function it would turn it back on. If it was already off the. Theif (wasLinked)
would be false the first time the state is checked.There are scroll functions. There is a scroll to track for the first one in the array selection referenced by
firstSelection [0]
on line 151 and another function in 162. Unfortunately it doesn’t seem to work consistently. I have now updated the code to be a bit more efficient in that because I noticed that in some cases the selection was lost after executing a scroll to track. I have fixed that and removed thetopEditTrack()
function as the new scroll is much more easier and efficient.
- FIn reply toSreejesh_Nair⬆:Forrester Savell @Forrester_Savell
Hello @Sreejesh_Nair
Thanks for writing/sharing this script. I was looking for ways to make the original remove inactive plugs/sends script faster by option-clicking and found this. You've pulled it off, great job!
I had a few issues running the script above, or the one from your package, perhaps things have changed with PT versions or our workflows are different. The script above breaks when trying to find the track height (line 90), so I replaced the function. I also had issues with line 17, so I've modified the
findInactiveItems
function. I also included a track filter so it only selects tracks with plugins/sends and brings them into view. Lastly I added some error handling so it restores the track heights if it fails.This script generally seems to work well for me. Keen to hear if it works for you or @danielkassulke or if any issues? My only issue is that it takes an inordinate amount of time to analyze an entire session (i actually haven't let it run long enough to test it as its +15 minutes). I find it quicker to batch select tracks, so wondering if you have the same issue?
const ignorePlugins = ["Melodyne", "Auto-Tune"] const proToolsMenuOptions = ["Inserts A-E", "Inserts F-J", "Sends A-E", "Sends F-J"]; function closeDialog() { const titles = ['Remove', 'Change']; titles.forEach(title => { if (sf.ui.proTools.confirmationDialog.invalidate().buttons.whoseTitle.is(title).first.exists) { sf.ui.proTools.confirmationDialog.buttons.whoseTitle.is(title).first.elementClick(); sf.ui.proTools.confirmationDialog.elementWaitFor({ waitType: "Disappear" }); } }); } function hideAllFloatingWindows() { //Hide all floating windows sf.ui.proTools.menuClick({ menuPath: ["Window", "Hide All Floating Windows"], targetValue: "Enable", }); } function scrollToTrackNamed(trackName, selectedTracks) { // Open scroll to track dialog and enter track name sf.ui.proTools.menuClick({ menuPath: ['Track', 'Scroll to Track...'] }); var confirmationDialogWin = sf.ui.proTools.confirmationDialog; confirmationDialogWin.elementWaitFor(); confirmationDialogWin.textFields.first.elementSetTextFieldWithAreaValue({ value: trackName }); confirmationDialogWin.buttons.whoseTitle.is('OK').first.elementClick(); confirmationDialogWin.elementWaitFor({ waitType: 'Disappear' }); //Re-select originally selected tracks sf.ui.proTools.trackSelectByName({ names: selectedTracks }) }; function saveCurrentTrackView() { sf.ui.proTools.appActivateMainWindow(); sf.keyboard.press({ keys: "numpad enter", }); sf.ui.proTools.newMemoryLocationDialog.elementWaitFor(); //Wait for the Mem Location dialog to appear and assign it to the memLocDlg variable var memLocDlg = sf.ui.proTools.dialogWaitForManual({ dialogTitle: 'New Memory Location' }).dialog; // We only want to store the track visibilty so clear all check boxes and then check the appropriate one var checkBoxes = memLocDlg.getElements("AXChildren").filter(function (e) { return e.fullRole == "AXCheckBox" }); for (var i = 0; i < 6; i++) { checkBoxes[i].checkboxSet({ targetValue: "Disable", }); } checkBoxes[2].checkboxSet({ targetValue: "Enable", }); checkBoxes[3].checkboxSet({ targetValue: "Enable", }); //Name the Location Track Visibilty sf.ui.proTools.newMemoryLocationDialog.textFields.allItems[0].elementSetTextFieldWithAreaValue({ value: "Track Visibility", }); //Get the Location Number and pass it back so we can delete it when we're done var locationNumber = sf.ui.proTools.newMemoryLocationDialog.textFields.allItems[1].value.value; // Set time Properties to none sf.ui.proTools.newMemoryLocationDialog.radioButtons.allItems[2].elementClick(); //Create the Location sf.ui.proTools.newMemoryLocationDialog.buttons.whoseTitle.is('OK').first.elementClick(); sf.ui.proTools.newMemoryLocationDialog.elementWaitFor({ waitType: "Disappear", }); return locationNumber; } function recallAndDeleteTrackView(locationNumber) { sf.ui.proTools.memoryLocationsGoto({ memoryLocationNumber: Number(locationNumber), }); sf.ui.proTools.memoryLocationsShowWindow(); sf.ui.proTools.memoryLocationsWindow.popupButtons.whoseTitle.is('Memory Locations').first.popupMenuSelect({ menuSelector: items => items.filter(i => i.names[0].match(/^Delete \"/))[0] }); sf.ui.proTools.confirmationDialog.buttons.whoseTitle.is("Yes").first.elementClick(); sf.ui.proTools.confirmationDialog.elementWaitFor({ waitType: "Disappear", }); sf.ui.proTools.menuClick({ menuPath: ["Window", "Memory Locations"], targetValue: 'Disable' //or 'Enable' or 'Toggle' }); sf.ui.proTools.memoryLocationsWindow.elementWaitFor({ waitType: "Disappear", }); } function findInactiveItems(tracks) { let map = { inserts: {}, sends: {} }; let elements = { inserts: [], sends: [] }; const getFrozenTrackState = (trackName) => { const track = sf.ui.proTools.trackGetByName({ name: trackName.normalizedTrackName }).track; const freezeBtn = track.buttons.whoseTitle.is("Frozen"); return freezeBtn ? !freezeBtn.exists : true; }; tracks.forEach(track => { if (track && (getFrozenTrackState(track))) { for (let i = 0; i < track.insertSelectorButtons.length; i++) { const button = track.insertSelectorButtons[i]; const insertBtn = track.insertButtons[i]; const plugName = insertBtn.value.invalidate().value; let plugExists = ignorePlugins.some(p => plugName.includes(p)); if (button.exists && button.invalidate().value.value.startsWith("inactive") && (plugExists != true)) { const slot = i + 1; if (!map.inserts[slot]) { map.inserts[slot] = []; elements.inserts.push(slot); } map.inserts[slot].push(track.normalizedTrackName); } } } for (let i = 0; i < track.sendSelectorButtons.length; i++) { const button = track.sendSelectorButtons[i]; if (button.exists && button.invalidate().value.value.startsWith("inactive")) { const slot = i + 1; if (!map.sends[slot]) { map.sends[slot] = []; elements.sends.push(slot); } map.sends[slot].push(track.normalizedTrackName); } } }) return { map, elements }; } function removeItems(names, type, slot) { sf.ui.proTools.trackSelectByName({ names: names, deselectOthers: true }); const original = sf.ui.proTools.selectedTrackNames; if (original.length > 0) { sf.ui.proTools.trackGetByName({ name: original[0] }).track.trackScrollToView(); } sf.ui.proTools.trackSelectByName({ names: names, deselectOthers: true }); sf.ui.proTools.selectedTrack.trackInsertOrSendSelect({ insertOrSend: type, pluginNumber: parseInt(slot, 10), selectForAllSelectedTracks: true, pluginPath: type === 'Insert' ? ['no insert'] : ['no send'] }); sf.wait({ intervalMs: 500 }); closeDialog(); } function batchRemove(map, elements, option) { if (option === "both" || option === "plugins") { map.inserts = map.inserts || {}; elements.inserts.forEach(slot => { if (map.inserts[slot]) { removeItems(map.inserts[slot], 'Insert', slot); } }); } if (option === "both" || option === "sends") { map.sends = map.sends || {}; elements.sends.forEach(slot => { if (map.sends[slot]) { removeItems(map.sends[slot], 'Send', slot); } }); } } function setTrackHeights(size) { const selectedTrackH = sf.ui.proTools.selectedTrackHeaders[0] const hasHeaderHeightPopup = selectedTrackH.frame.h <= 79 try { selectedTrackH.popupMenuSelect({ anchor: "MidRight", relativePosition: { x: - 10, y: 0 }, menuPath: [size], isOption: true, onError: "Continue", }) } catch (err) { // Try header left poput if (hasHeaderHeightPopup) { sf.ui.proTools.selectedTrack.popupButtons.allItems[2].popupMenuSelect({ menuPath: ["Track Height", size], isOption: true, }); }; }; }; function activateProToolsMenus(menuOptions) { menuOptions.forEach(menuOption => { sf.ui.proTools.menuClick({ menuPath: ["View", "Edit Window Views", menuOption], targetValue: "Enable" }); }); } function selectAllVisibleTracks() { sf.ui.proTools.trackDeselectAll(); const topOfEditWindow = sf.ui.proTools.mainWindow.timelineFocusButton.frame.y; const topTrack = sf.ui.proTools.visibleTrackHeaders.filter(h => h.frame.y >= topOfEditWindow)[0]; topTrack.titleButton.mouseClickElement({ isOption: true }); } function filterTracks(initialSelection) { sf.ui.proTools.appActivateMainWindow(); //Create array of tracks with Inserts & Sends, from initially selected tracks var allVisibleSelectedTracks = initialSelection .filter(t => t != null && ( t.title.value.includes("Audio Track") || t.title.value.includes("Aux Track") || t.title.value.includes('Inst Track') || t.title.value.includes('Routing Folder Track') || t.title.value.includes('Master Track')) ) //log(allVisibleSelectedTracks); return allVisibleSelectedTracks } function onScreenMessage(job, x) { sf.interaction.displayDialog({ prompt: job, giveUpAfterSeconds: x }); }; function startScript() { sf.ui.proTools.appActivate(); //sf.ui.proTools.mainWindow.invalidate(); //Get the current window name const focusedWindow = sf.ui.proTools.focusedWindow.title.invalidate().value.split(" ", 1)[0].slice(0, -1); //If not already in the Edit window, switch to the Edit window if (focusedWindow !== 'Edit') { try { sf.ui.proTools.menuClick({ menuPath: ['Window', 'Edit'] }); } catch (err) { log('Could not focus Edit Window') } } let userViewNum = saveCurrentTrackView() hideAllFloatingWindows() let restoreTracks = sf.ui.proTools.selectedTrackHeaders.filter(t => t != null) // Check if Track List is Open const isTrackListOpen = () => sf.ui.proTools.getMenuItem('View', 'Other Displays', 'Track List').isMenuChecked const initialState = isTrackListOpen() // // Open Track List sf.ui.proTools.menuClick({ menuPath: ['View', 'Other Displays', 'Track List'], targetValue: "Enable" }) //Ensure Link Track and Edit Selection is enabled const wasLinked = sf.ui.proTools.getMenuItem('Options', 'Link Track and Edit Selection').isMenuChecked; if (wasLinked) { sf.ui.proTools.menuClick({ menuPath: ["Options", "Link Track and Edit Selection"], }); } //Ensure Inserts and Sends are visible in Edit Window activateProToolsMenus(proToolsMenuOptions); sf.ui.proTools.invalidate(); //Hide Inactive Tracks for clarity sf.ui.proTools.trackOpenListPopupMenu().popupMenu.menuClickPopupMenu({ menuPath: ["Hide", "Inactive Tracks"], }); //Store initial track selection var firstSelection = sf.ui.proTools.invalidate().trackGetSelectedTracks().trackHeaders.filter(t => t != null); //If no tracks selected, ask if entire session is to be processed if (firstSelection.length == 0) { var procSession = sf.interaction.displayDialog({ buttons: ["Cancel", "Proceed"], defaultButton: 'Proceed', prompt: "No tracks selected, proceed with entire session?", }).button; if (procSession == 'Proceed') { sf.ui.proTools.trackSelectByName({ names: sf.ui.proTools.visibleTrackNames }); } else throw 0; var firstSelection = sf.ui.proTools.invalidate().trackGetSelectedTracks().trackHeaders.filter(t => t != null); } try { //Find and store tracks that have inactive Inserts and Sends (and isn't Frozen) let tracksForProcessing = filterTracks(firstSelection) if (tracksForProcessing.length == 0) { alert("There was an error, no tracks selected. Script cancelled."); throw 0; } //Select the suitable tracks ready for analysis scrollToTrackNamed(tracksForProcessing[0].normalizedTrackName, tracksForProcessing.map(t => t.normalizedTrackName)) onScreenMessage("Analysing session for inactive plugins/sends. Please wait...", 3); const { map, elements } = findInactiveItems(tracksForProcessing); onScreenMessage("Analysis complete. Proceeding.", 2); //End script if no inactive Inserts or Sends found if (elements.inserts.length === 0 && elements.sends.length === 0) { onScreenMessage("No inactive plugins or sends found.", 1); throw 0; } //Set track height to small so can visually see more of session - mini is too small setTrackHeights('small'); ///////////////////////////////////////////////////////////////////// //Currently have removed options to remove only Inserts or only Sends - Script does both. Plan to update with Command Template ///////////////////////////////////////////////////////////////////// let option; /* //if (chosenOption === "Remove all Inactive Plugins and Sends") { // option = "both"; //} else if (chosenOption === "Remove all Inactive Plugins") { if (elements.inserts.length === 0) { alert("No inactive plugins found."); return; } // option = "plugins"; //} else if (chosenOption === "Remove all Inactive Sends") { if (elements.sends.length === 0) { alert("No inactive sends found."); return; } // option = "sends"; //} */ option = 'both' batchRemove(map, elements, option); if (wasLinked) { sf.ui.proTools.menuClick({ menuPath: ["Options", "Link Track and Edit Selection"], }); } } catch (err) { //Handle the error log('Could not remove plugins/sends'); } recallAndDeleteTrackView(userViewNum) if (restoreTracks.length != 0) { scrollToTrackNamed(restoreTracks[0].normalizedTrackName, restoreTracks.map(t => t.normalizedTrackName)) } else { scrollToTrackNamed(firstSelection[0].normalizedTrackName, firstSelection.map(t => t.normalizedTrackName)) } // Set Track List to Initial State if (initialState != isTrackListOpen()) { sf.ui.proTools.menuClick({ menuPath: ['View', 'Other Displays', 'Track List'], targetValue: "Disable" }) }; onScreenMessage("Script Finished", 1) } startScript()
- SSreejesh Nair @Sreejesh_Nair
This is great. It could be that things may have changed with a newer version of PT. I’ll give this a swirl!
- In reply toForrester_Savell⬆:FForrester Savell @Forrester_Savell
Hi @Sreejesh_Nair and anyone else interested.
I've done a major upgrade to my previous version, available in the code above.
It's a lot more robust.
- Tracks can be bulk selected from Track List or Edit Window and it will exclude inactive, hidden or non-Insert/Send related tracks, so they don't interfere with the process. I've based its functionality on my need for selecting an entire finished session and removing inactive items, ready for archiving.
- I've added an option to ignore specific user defined plugins. Add them to the
const ignorePlugins
array at the top of the script. - I've also removed the option to choose between Inserts or Sends, it just does both for now. I can add that later as a Command Template.
- Will remove sends on Frozen tracks, but will ignore any inserts
- In reply toForrester_Savell⬆:FForrester Savell @Forrester_Savell
Thanks for the feedback. It was due to the Track List on the left not being visible. I've updated the script to ensure that it opens before running.
Let me know in this thread if you have any further issues with the script.
Cheers
Forrester
- SSean McDonald @Sean_McDonald5
Hey Forrester!
Is the most recent/ updated version the last one in this thread dated jun 13?thanks!
- FForrester Savell @Forrester_Savell
Yes, rather than reposting, I've updated the code above Remove Inactive Inserts or Sends or both #post-6
- SSean McDonald @Sean_McDonald5
Forrester.......
that was AMAZING!!
worked perfectly, and VERY quickly!Can you share your Venmo/ Paypal etc? would love to send some dough.
thanks!
- FForrester Savell @Forrester_Savell
Hey @Sean_McDonald5
Glad that it worked for you. No need for payment :)
- In reply toForrester_Savell⬆:SSean McLaughlin @Sean_McLaughlin
Hi Forrester,
First, thanks for doing this! Second, I'm running into an issue where it can't open the Track list popup menu (line 305).
- FForrester Savell @Forrester_Savell
Hey @Sean_McLaughlin
Are you still having trouble with this? If you haven't already, try using the updated code posted above. This looks like the same issue the other Sean above was having, which I've since fixed.
- SSean McLaughlin @Sean_McLaughlin
I'm still having the same issue with the last code in this thread.
- SSreejesh Nair @Sreejesh_Nair
Does it happen if you run it on the Edit window? I think this may be because it’s the mix window?
- SSean McLaughlin @Sean_McLaughlin
Thank you! That didn't give me the same error code, but now it seems to start and not finish. I tried running the script on one track and it didn't remove any of the inactive plugins.
- FForrester Savell @Forrester_Savell
Hey @Sean_McLaughlin
I've revised the code again to work even if you have the Mix Window open. Let me know if you're still having issues and perhaps capture a video so I can see what's happening.
Thanks for the feedback too!
- SIn reply toSreejesh_Nair⬆:Sreejesh Nair @Sreejesh_Nair
function batchRemove(map, elements, option) { let processedItems = 0, notificationID = sf.system.newGuid().guid, totalItems = elements.inserts.length + elements.sends.length; const removeFromSlot = (slot, type, map) => { removeItems(map[slot], type, slot); // Notify progress of removal sf.interaction.notify({ uid: notificationID, title: "Removing Inactive Items", progress: (++processedItems) / totalItems, message: `Removing ${type}s: ${processedItems} of ${totalItems}` }); }; if (option === "both" || option === "plugins") elements.inserts.forEach(slot => removeFromSlot(slot, 'Insert', map.inserts, elements.inserts)); if (option === "both" || option === "sends") elements.sends.forEach(slot => removeFromSlot(slot, 'Send', map.sends, elements.sends)); sf.interaction.notify({ uid: notificationID, title: "Removing Inactive Items", progress: 100, message: `Removal complete.` }); }
This is the new batch remove that I use with the notification that shows the progress of the removal as well. The option is because I have an interaction list that asks for all, or just sends or plugins.
- FForrester Savell @Forrester_Savell
Nice one @Sreejesh_Nair , this will be handy. Does this work in your original script or my alternate code?
When I swap it out for my
Batch Rename
function in my script, I'm getting this error. Haven't had a chance to dig into it, but perhaps you know what's happening?- SSreejesh Nair @Sreejesh_Nair
Its because I defined elements here
function findInactiveItems(tracks) { let map = { inserts: {}, sends: {} }, elements = { inserts: [], sends: [] }, notificationID = sf.system.newGuid().guid; tracks.forEach((track, index) => { // Notify progress of processing tracks sf.interaction.notify({ uid: notificationID, title: "Processing Automation Data", progress: (index + 1) / tracks.length, message: `Analysing Session: ${index + 1} of ${tracks.length} tracks` }); if (track) { // Process insert buttons track.insertSelectorButtons.forEach((button, i) => { if (button.exists && button.invalidate().value.value.startsWith("inactive")) { const slot = i + 1; if (!map.inserts[slot]) { map.inserts[slot] = []; elements.inserts.push(slot); } map.inserts[slot].push(track.normalizedTrackName); } }); // Process send buttons track.sendSelectorButtons.forEach((button, i) => { if (button.exists && button.invalidate().value.value.startsWith("inactive")) { const slot = i + 1; if (!map.sends[slot]) { map.sends[slot] = []; elements.sends.push(slot); } map.sends[slot].push(track.normalizedTrackName); } }); } }); return { map, elements }; }
Here is my full code that I use now. You could modify it to include the changes you made
// Closes Pro Tools confirmation dialogs if they contain "Remove" or "Change" buttons function closeDialog() { ['Remove', 'Change'].forEach(title => { const button = sf.ui.proTools.confirmationDialog.invalidate().buttons.whoseTitle.is(title).first; if (button.exists) { button.elementClick(); sf.ui.proTools.confirmationDialog.elementWaitFor({ waitType: "Disappear" }); } }); } // Finds inactive inserts and sends in the provided tracks and maps them function findInactiveItems(tracks) { let map = { inserts: {}, sends: {} }; let elements = { inserts: [], sends: [] }; let notificationID = sf.system.newGuid().guid; tracks.forEach((track, index) => { // Notify progress of processing tracks sf.interaction.notify({ uid: notificationID, title: "Processing Automation Data", progress: (index + 1) / tracks.length, message: `Analysing Session: ${index + 1} of ${tracks.length} tracks` }); if (track) { // Process insert buttons track.insertSelectorButtons.forEach((button, i) => { if (button.exists && button.invalidate().value.value.startsWith("inactive")) { const slot = i + 1; if (!map.inserts[slot]) { map.inserts[slot] = []; elements.inserts.push(slot); } map.inserts[slot].push(track.normalizedTrackName); } }); // Process send buttons track.sendSelectorButtons.forEach((button, i) => { if (button.exists && button.invalidate().value.value.startsWith("inactive")) { const slot = i + 1; if (!map.sends[slot]) { map.sends[slot] = []; elements.sends.push(slot); } map.sends[slot].push(track.normalizedTrackName); } }); } }); return { map, elements }; } // Removes items (inserts or sends) from the specified tracks and slots function removeItems(names, type, slot) { sf.keyboard.press({ keys: "home" }); sf.ui.proTools.trackSelectByName({ names, deselectOthers: true }); const original = sf.ui.proTools.selectedTrackNames; if (original.length > 0) { sf.ui.proTools.trackGetByName({ name: original[0] }).track.trackScrollToView(); } sf.ui.proTools.trackSelectByName({ names, deselectOthers: true }); sf.ui.proTools.selectedTrack.trackInsertOrSendSelect({ insertOrSend: type, pluginNumber: parseInt(slot, 10), selectForAllSelectedTracks: true, pluginPath: type === 'Insert' ? ['no insert'] : ['no send'] }); closeDialog(); sf.wait({ intervalMs: 250 }); } // Batch removes inactive inserts or sends based on the selected option function batchRemove(map, elements, option) { let processedItems = 0; let notificationID = sf.system.newGuid().guid; const totalItems = elements.inserts.length + elements.sends.length; const removeFromSlot = (slot, type, map) => { removeItems(map[slot], type, slot); // Notify progress of removal sf.interaction.notify({ uid: notificationID, title: "Removing Inactive Items", progress: (++processedItems) / totalItems, message: `Removing ${type}s: ${processedItems} of ${totalItems}` }); }; if (option === "both" || option === "plugins") { elements.inserts.forEach(slot => removeFromSlot(slot, 'Insert', map.inserts)); } if (option === "both" || option === "sends") { elements.sends.forEach(slot => removeFromSlot(slot, 'Send', map.sends)); } sf.interaction.notify({ uid: notificationID, title: "Removing Inactive Items", progress: 100, message: `Removal complete.` }); } // Sets the track size for the selected track function setTrackSize(size) { const selectedTrackH = sf.ui.proTools.selectedTrackHeaders[0]; try { // Try setting track size using the right popup menu selectedTrackH.popupMenuSelect({ anchor: "MidRight", relativePosition: { x: -10, y: 0 }, menuPath: [size], isOption: true, onError: "Continue" }); } catch (err) { // If the right popup menu fails, try the left popup menu if (selectedTrackH.frame.h <= 79) { sf.ui.proTools.selectedTrack.popupButtons.allItems[2].popupMenuSelect({ menuPath: ["Track Height", size], isOption: true }); } } } // Activates the specified Pro Tools menu options function activateProToolsMenus(menuOptions) { menuOptions.forEach(menuOption => sf.ui.proTools.menuClick({ menuPath: ["View", "Edit Window Views", menuOption], targetValue: "Enable" }) ); } // Define Pro Tools menu options to be activated const proToolsMenuOptions = ["Inserts A-E", "Inserts F-J", "Sends A-E", "Sends F-J"]; // Main script function that initiates the process based on user selection function startScript() { // User selection prompt const userSelection = sf.interaction.selectFromList({ items: ["Remove all Inactive Plugins and Sends", "Remove all Inactive Plugins", "Remove all Inactive Sends"], allowMultipleSelections: false, prompt: "Choose what you want removed", title: "Inactive Cleaner" }).list; if (userSelection.length === 0) return alert("No option selected. Exiting script."); let chosenOption = userSelection[0]; sf.ui.proTools.appActivate(); sf.ui.proTools.mainWindow.invalidate(); const wasLinked = sf.ui.proTools.getMenuItem('Options', 'Link Track and Edit Selection').isMenuChecked; if (wasLinked) sf.ui.proTools.menuClick({ menuPath: ["Options", "Link Track and Edit Selection"] }); if (!sf.ui.proTools.mainWindow.title.invalidate().value.startsWith("Edit: ")) { sf.ui.proTools.menuClick({ menuPath: ["Window", "Edit"] }); } activateProToolsMenus(proToolsMenuOptions); sf.ui.proTools.invalidate(); let tracksForProcessing = sf.ui.proTools.selectedTrackHeaders; let firstSelection = sf.ui.proTools.selectedTrackNames; if (tracksForProcessing.length !== 0) { sf.ui.proTools.trackGetByName({ name: firstSelection[0] }).track.trackScrollToView(); sf.ui.proTools.trackSelectByName({ names: firstSelection }); setTrackSize('small'); } else { if (!confirm("No tracks are selected. Would you like to proceed with all visible active tracks?")) { return alert("Operation cancelled."); } firstSelection = sf.ui.proTools.visibleTrackNames; sf.ui.proTools.trackGetByName({ name: firstSelection[0] }).track.trackScrollToView(); const activeTrackNames = sf.app.proTools.tracks.invalidate().allItems.reduce((activeTrackNames, track) => { if (!track.isInactive) activeTrackNames.push(track.name); return activeTrackNames; }, []); sf.app.proTools.selectTracksByName({ trackNames: activeTrackNames }); setTrackSize('small'); tracksForProcessing = sf.ui.proTools.selectedTrackHeaders; } const { map, elements } = findInactiveItems(tracksForProcessing); if (elements.inserts.length === 0 && elements.sends.length === 0) { return alert("No inactive plugins or sends found."); } let option = chosenOption === "Remove all Inactive Plugins and Sends" ? "both" : chosenOption === "Remove all Inactive Plugins" ? (elements.inserts.length === 0 ? (alert("No inactive plugins found."), undefined) : "plugins") : (elements.sends.length === 0 ? (alert("No inactive sends found."), undefined) : "sends"); if (option) batchRemove(map, elements, option); if (wasLinked) sf.ui.proTools.menuClick({ menuPath: ["Options", "Link Track and Edit Selection"] }); sf.ui.proTools.trackSelectByName({ names: firstSelection, deselectOthers: true }); alert("Operation completed."); } // Start the script startScript();
- FForrester Savell @Forrester_Savell
That's very cool!!
I'll try to implement it in my code, as I still run into issues with frozen tracks (where an inactive plugin is sandwiched between frozen ones) and some dialogs like the image.
But wow, that is a handy feature add.
- SSreejesh Nair @Sreejesh_Nair
If you want to skip frozen tracks, you can change line 189 to
const activeTrackNames = sf.app.proTools.tracks.invalidate().allItems.reduce((activeTrackNames, track) => { if (!track.isInactive && !track.isFrozen) activeTrackNames.push(track.name); return activeTrackNames; }, [])
- In reply toSreejesh_Nair⬆:SSean McLaughlin @Sean_McLaughlin
Success! Thanks so much for doing this!
- JIn reply toSreejesh_Nair⬆:James McRay Johnson III @James_McRay_Johnson
This is amazing, and thank you to all involved.
I am dedicated to using my mouse as little as possible, as clicking through tiny sub-menus hidden behind tiny little dots in the Pro Tools Edit window is taking its toll on my mousing hand.
One thought/proposal: it would be REALLY nice to have the popup window auto-focus, instead of needing to click on the popup window with my mouse to bring into focus. I am going to dive into this when I have a chance, just wanted to make sure that I am not missing something that has already been implemented.
- SIn reply toSreejesh_Nair⬆:Sreejesh Nair @Sreejesh_Nair
Which pop up window are you referring to?
- JJames McRay Johnson III @James_McRay_Johnson
The popup that shows the three options to choose from: sends, plugins, or both. Since it it not initially focused, one must use the mouse to focus it.
Once it is focused, I can then use the arrow and enter keys to initiate the plugin removal. I just don't want to use the mouse to focus it.
- SSreejesh Nair @Sreejesh_Nair
Ah that. It seems to be a soundflow thing. Another workaround is to create a template and make three presets one for each of the options. That way you can trigger the choice from streamdeck or a keyboard shortcut without a mouse interaction.
- JJames McRay Johnson III @James_McRay_Johnson
Ok, I'll give it a try.