Smart Track Duplicator - Help With Script
Hi There!
@raphaelsepulveda had been talking about this process in an instagram post, but I have a script here that i've been using for a while to duplicate tracks in a smart way.
@raphaelsepulveda this script stopped working at line 64 today (bulk rename) but instead of troubleshooting it too much i wondered if you had a better solution at this point!
sf.ui.proTools.appActivate();
sf.ui.proTools.invalidate();
// Place the following two functions at the top of your script:
// #1
function mainWindowStatus() {
if (sf.ui.proTools.getMenuItem('Window', 'Mix').isMenuChecked) {
sf.ui.proTools.menuClick({
menuPath: ["Window", "Edit"],
});
return "Mix";
} else {
return "Edit";
}
}
//#2
function returnToStartingMainWIndow(mainWindow) {
if (mainWindow =="Mix") {
sf.ui.proTools.menuClick({
menuPath: ["Window", "Mix"],
});
}
}
/// Place this line at the top of your script
let startingWindow = mainWindowStatus();
function main() {
sf.ui.proTools.appActivateMainWindow();
sf.ui.proTools.invalidate();
var numberOfDups = Number(prompt('How many duplicates?', '1'));
let tracksToDup = sf.ui.proTools.selectedTrackNames;
sf.ui.proTools.trackDuplicateSelected({
duplicateActivePlaylist: false,
duplicateAlternatePlaylists: false,
duplicateAutomation: false,
duplicateGroupAssignments: false,
duplicateInserts: true,
duplicateSends: true,
insertAfterLastSelectedTrack: true,
numberOfDuplicates: numberOfDups
});
const dupTracks = sf.ui.proTools.invalidate().selectedTrackNames;
//Rename Tracks
tracksToDup.forEach(trackName => {
const trackDups = dupTracks.filter(tn => tn.startsWith(trackName))
sf.ui.proTools.trackSelectByName({ names: [trackName, ...trackDups] });
sf.ui.proTools.selectedTrack.trackBulkRenameNumerical();
});
};
main();
// end your script with this line
returnToStartingMainWIndow(startingWindow);
- Raphael Sepulveda @raphaelsepulveda2024-08-20 04:05:50.325Z
@Philip_weinrobe, yeah! We can definitely modernize this one by leveraging the PT SDK.
We can now rename tracks directly so we don't need to do the Edit/Mix window switch-a-roo anymore.The renaming function from the script I showed on Instagram is custom to my naming scheme. All that means is that I have it rename tracks differently depending on what the original name is. For this script, I've recreated the
trackBulkRenameNumerical
function so we can use it with the PT SDK. It should work the same way you're used to. Let me know if it misbehaves!function duplicateTracksSmartRename() { /** Renames tracks by removing ".dup#" and numbering sequentially. * @param { string[] } trackNames */ function trackBulkRenameNumerical(trackNames) { trackNames.forEach((trackName, index) => { const newName = trackName .replace(/.dup.*/, "") // Remove ".dup" and anything after that .replace(/\s?\d+$/, "") // Remove numbers at end of name + " " // Add space + String(index + 1); // Add number if (trackName === newName) return; sf.app.proTools.renameTrack({ oldName: trackName, newName }) }); } const numberOfDups = Number( sf.interaction.displayDialog({ title: "Duplicate Tracks (Smart Rename)", prompt: "How many duplicates?", defaultAnswer: "1", buttons: ["Cancel", "Ok"], defaultButton: "Ok", cancelButton: "Cancel", }).text ); sf.ui.proTools.appActivateMainWindow(); sf.ui.proTools.mainWindow.invalidate(); const tracksToDup = sf.ui.proTools.selectedTrackNames; // Duplicate tracks sf.ui.proTools.trackDuplicateSelected({ duplicateActivePlaylist: false, duplicateAlternatePlaylists: false, duplicateAutomation: false, duplicateGroupAssignments: false, duplicateInserts: true, duplicateSends: true, insertAfterLastSelectedTrack: true, numberOfDuplicates: numberOfDups }); sf.ui.proTools.mainWindow.invalidate(); const dupTracks = sf.ui.proTools.selectedTrackNames; //Rename Tracks tracksToDup.forEach(trackName => { const trackDups = dupTracks.filter(tn => tn.startsWith(trackName)) trackBulkRenameNumerical([trackName, ...trackDups]); }); } duplicateTracksSmartRename();
- PPhilip weinrobe @Philip_weinrobe
amazing! seems to work great from the jump.
thanks raphael!- PPhilip weinrobe @Philip_weinrobe
actually, one small thing:
my old script and this new one both have one inherent flaw: they don't know if a track name already exists in the session. i seem to recall you had solved that in your flow.
any chance of including that error checking here?thanks!
PhilipRaphael Sepulveda @raphaelsepulveda2024-08-22 16:04:14.549Z
Yeah, no problem. I'll take care of this over the weekend!
- PPhilip weinrobe @Philip_weinrobe
fyi this stopped working?
here's some error code i got27.09.2024 10:36:35.86 <info> [Backend]: !! Command Error: Duplicate Tracks [user:default:cl3ylhnjk0002ie10j6w7ykd0]: Could not find running app with bundle id: 'com.avid.ProTools' (Duplicate Tracks: Line 32) << Command: Duplicate Tracks [user:default:cl3ylhnjk0002ie10j6w7ykd0] 27.09.2024 10:36:36.02 <info> [Backend]: [KeyboardInterfaceController] Invalidating PT memlocs due to numpad enter 27.09.2024 10:36:37.27 <info> [EditorWindow:Renderer]: Active Focus Container: commandsPage Line 33963 file:///Applications/SoundFlow.app/Contents/Helpers/SoundFlow.app/Contents/Resources/app.asar/dist/editor.js
Raphael Sepulveda @raphaelsepulveda2024-09-27 18:29:45.971Z
At
sf.ui.proTools.appActivateMainWindow()
? Try this.- Restart SoundFlow
- Make sure nothing is covering the top left corner of the edit window (where the traffic lights are). That will throw an error with
appActivateMainWindow
every time. - If it's still cranky, replace it with
sf.ui.proTools.appActivate()
- AAdam Lilienfeldt @Adam_Lilienfeldt
Hi Phil and Raphael,
I stumbled upon this post and decided to update your script and modify it to work with my naming conventions.
Maybe you'll find a use for it - it should be quite easy to modify too. I still need to do some testing on it, but so far it seems to work quite well.The script gets all track names from the session and finds the correct numbering for the new track(s).
It also accounts for playlist numbering (I tend to name my tracks track name.01 from the get go). So if you duplicate a track with playlist numbering, it will add .01 to that track name.If you have tracks called Guitar 1, Guitar 2, Guitar 3, Guitar 4 and decide to duplicate Guitar 1, it will call that track Guitar 5.
Same logic applies with playlist numbering - if you have Guitar 1.05, Guitar 2.01, Guitar 3.11 and duplicate Guitar 1.05, it becomes Guitar 4.01.function duplicateTracksSmartRename() { const numberOfDups = Number( sf.interaction.displayDialog({ title: "Duplicate Tracks (Smart Rename)", prompt: "How many duplicates?", defaultAnswer: "1", buttons: ["Cancel", "Ok"], defaultButton: "Ok", cancelButton: "Cancel", }).text ); sf.ui.proTools.appActivateMainWindow(); sf.ui.proTools.mainWindow.invalidate(); const tracksToDup = sf.ui.proTools.selectedTrackNames; // Duplicate tracks sf.ui.proTools.trackDuplicateSelected({ duplicateActivePlaylist: false, duplicateAlternatePlaylists: false, duplicateAutomation: false, duplicateGroupAssignments: false, duplicateInserts: true, duplicateSends: true, insertAfterLastSelectedTrack: true, numberOfDuplicates: numberOfDups }); sf.ui.proTools.mainWindow.invalidate(); // Get the currently selected tracks (these are the newly created duplicates) const newlyCreatedTracks = sf.ui.proTools.selectedTrackNames; //log(`Original tracks to duplicate: ${tracksToDup.join(', ')}`); //log(`Newly created tracks (selected): ${newlyCreatedTracks.join(', ')}`); // Get ALL tracks in the session to check for existing numbers const allTracksInSession = sf.ui.proTools.trackGetAllTracks().names; //log(`All tracks in session: ${allTracksInSession.join(', ')}`); // Keep track of numbers we've already assigned to avoid conflicts const assignedNumbers = {}; // Rename only the newly created duplicate tracks tracksToDup.forEach((originalTrackName, originalIndex) => { //log(`Processing original track: ${originalTrackName}`); // Check if the original track has playlist numbering (.XX format) const hasPlaylistNumber = /\.\d{2}$/.test(originalTrackName); //log(`Original track has playlist numbering: ${hasPlaylistNumber}`); // Get the base name from the original track let baseName = originalTrackName.replace(/\.\d{2}$/, "").replace(/\s?\d+$/, ""); //log(`Base name: ${baseName}`); // Find all existing track numbers for this base name in the entire session const existingNumbers = allTracksInSession .filter(trackName => { // Get the base name of this track const trackBaseName = trackName.replace(/\.dup\d+/, "").replace(/\.\d{2}$/, "").replace(/\s?\d+$/, ""); return trackBaseName === baseName; }) .map(trackName => { // Extract the number from the track name (after removing .dup and playlist) const withoutDupAndPlaylist = trackName.replace(/\.dup\d+/, "").replace(/\.\d{2}$/, ""); const numberMatch = withoutDupAndPlaylist.match(/\s?(\d+)$/); return numberMatch ? parseInt(numberMatch[1]) : 1; }); // Include previously assigned numbers from this session const allUsedNumbers = [...existingNumbers, ...(assignedNumbers[baseName] || [])]; const highestExistingNumber = allUsedNumbers.length > 0 ? Math.max(...allUsedNumbers) : 0; //log(`Existing numbers for ${baseName}: ${existingNumbers.join(', ')}`); //log(`Previously assigned numbers: ${(assignedNumbers[baseName] || []).join(', ')}`); //log(`Highest existing number: ${highestExistingNumber}`); // Initialize the assigned numbers array for this base name if it doesn't exist if (!assignedNumbers[baseName]) { assignedNumbers[baseName] = []; } // Find the duplicates that were created from this specific original track const duplicatesFromThisTrack = newlyCreatedTracks.filter(trackName => { // Check if this duplicate was created from the current original track // Remove .dup# but keep the playlist number const withoutDup = trackName.replace(/\.dup\d+/, ""); const hasDup = trackName.includes('.dup'); const matches = withoutDup === originalTrackName; //log(`Checking track: "${trackName}"`); //log(` Without dup: "${withoutDup}"`); //log(` Has .dup: ${hasDup}`); //log(` Matches original: ${matches}`); //log(` Result: ${hasDup && matches}`); return hasDup && matches; }); //log(`Found ${duplicatesFromThisTrack.length} duplicates: ${duplicatesFromThisTrack.join(', ')}`); // Rename each duplicate, starting from the next available number duplicatesFromThisTrack.forEach((dupTrackName, dupIndex) => { const newNumber = highestExistingNumber + dupIndex + 1; // Track this number as assigned assignedNumbers[baseName].push(newNumber); // Only add playlist numbering if the original track had it const newName = hasPlaylistNumber ? baseName + " " + newNumber + ".01" : baseName + " " + newNumber; //log(`Renaming: "${dupTrackName}" -> "${newName}"`); if (dupTrackName === newName) { log("Names are identical, skipping rename"); return; } sf.app.proTools.renameTrack({ oldName: dupTrackName, newName: newName }); }); }); } duplicateTracksSmartRename();
Raphael Sepulveda @raphaelsepulveda2025-07-10 20:50:12.107Z
@Adam_Lilienfeldt, nice work!