Hey all - wondering whether something like this is possible... The scenario is you've just been handed lots of poorly named/organised files to arrange in your timeline, which all require manually inserting onto a new track. My dream is that after importing all audio, you could sort the clip list by file length, then, starting with the shortest clip lengths, extract each of those clips of identical length from the clip list with a top-to-bottom timeline drop order via spot to edit insertion, (thus creating a new track for each of those clips) then loop the command for the next shortest clip, until there are no clips left in the clip list with identical clip lengths.
Here's my starting point, with only a skeleton and lots of holes in the script. I guess I'm wondering if this is an achievable goal...
sf.ui.proTools.appActivateMainWindow();
sf.ui.proTools.menuClick({
menuPath: ["File","Import","Audio..."],
});
sf.ui.proTools.windows.whoseTitle.is("Open").first.splitGroups.first.splitGroups.first.children.whoseRole.is("AXBrowser").whoseDescription.is("column view").first.scrollAreas.first.elementWaitFor({
waitType: "Appear",
timeout: 2000,
});
sf.ui.proTools.windows.whoseTitle.is("Open").first.splitGroups.first.splitGroups.first.children.whoseRole.is("AXBrowser").whoseDescription.is("column view").first.scrollAreas.first.elementWaitFor({
waitType: "Disappear",
timeout: 120000,
});
sf.ui.proTools.windows.whoseTitle.is("Audio Import Options").first.elementWaitFor({
waitType: "Appear",
timeout: 2000,
});
sf.ui.proTools.windows.whoseTitle.is("Audio Import Options").first.radioButtons.whoseTitle.is("Clip List <Command> 'R'").first.mouseClickElement();
sf.ui.proTools.windows.whoseTitle.is("Audio Import Options").first.buttons.whoseTitle.is("OK").first.mouseClickElement();
sf.ui.proTools.mainWindow.popupButtons.first.popupMenuSelect({
menuPath: ["Sort by","Length"],
});
sf.ui.proTools.mainWindow.popupButtons.first.popupMenuSelect({
menuPath: ["Spot to Edit Insertion"],
- Kitch Membery @Kitch2022-11-23 03:30:18.362Z
Hi Daniel,
I'll take a look at this when I get a chance. :-)
- In reply todanielkassulke⬆:Kitch Membery @Kitch2022-11-28 10:00:55.587Z
Hi @danielkassulke,
Does this look like the workflow you want to achieve? There is a bunch to do in this script but I think I can mock something up for you.
-
Prompt for the folder that holds the audio files.
-
From the selected directory, create an object of the audio files that contains information on the files including the “File Name”, “File Length” and “Track Width”.
-
Import all the audio files from the folder into Pro Tools via File => Import => Audio…
-
Ensure the Clips List is visible.
-
Ensure that the Clips List is not in Find mode ie. Click “Clear Find” in the Clips List Popup.
-
Ensure The Clips List “Timeline Drop Order” is set to “Top-to-Bottom”.
-
For each set of clips with matching length do the following:
- For Each clip in the set =>
- Create a new track with matching track width.
- Select the clip in the Clips List.
- Spot the clip via “Spot to Edit Insertion” in the Clip List popup.
- For Each clip in the set =>
-
Repeat for each set of clips with matching lengths.
-
Alert when complete.
Let me know :-)
-
- DIn reply todanielkassulke⬆:danielkassulke @danielkassulke
Hi @Kitch ,
You're basically spot on, but with Ensure the Clips List is visible as a starting point, mostly because of the faffing associated with importing audio from multiple places at once. This would truly be one small step for Dan, but a giant step for Dankind
Kitch Membery @Kitch2022-12-02 10:36:49.262Z
Hi @danielkassulke,
Soz for the delay... this one ended up being a bit more complex than I first thought. It needs a bit of refactoring but should work as is. :-)
let clipsInfo; /** * @param {string} description * @param {string} path */ function setTrackPopupOption(description, path) { let win = sf.ui.proTools.windows.whoseTitle.is("New Tracks").first; if (win.popupButtons.whoseDescription.is(description).first.title.invalidate().value !== path) { win.popupButtons.whoseDescription.is(description).first.popupMenuSelect({ menuPath: [path], }, `Could not select the "${description}" : ${path}`); } } /** * @param {AxElement} numberField * @param {string} targetValue */ function setNumberFieldValue(numberField, targetValue) { if (numberField.value.invalidate().value.trim() !== targetValue) { const oldClipboardText = sf.clipboard.getText().text; try { const text = targetValue; sf.clipboard.setText({ text: text }); sf.ui.proTools.appActivateMainWindow(); numberField.elementClick(); sf.keyboard.press({ keys: "cmd+v", }); var i = 0; do { sf.wait({ intervalMs: 10 }); i++ if (i >= 20) { throw `Could not update the number field` } } while (numberField.value.invalidate().value !== targetValue); } finally { sf.clipboard.setText({ text: oldClipboardText }); } } } /** * @param {object} trackSettings * @param {number} trackSettings.numberOfNewTracks * @param {string} trackSettings.trackFormat * @param {string} trackSettings.trackType * @param {string} trackSettings.trackTimebase * @param {string} trackSettings.trackName * @param {boolean} trackSettings.clickCreate */ function createNewTrack(trackSettings) { const { numberOfNewTracks, trackFormat, trackType, trackTimebase, trackName, clickCreate } = trackSettings; sf.ui.proTools.menuClick({ menuPath: ["Track", "New..."] }); const win = sf.ui.proTools.windows.whoseTitle.is('New Tracks').first; win.elementWaitFor(); const numberOfNewTracksField = win.textFields.whoseTitle.is("Number of new tracks").first; //Set number of tracks setNumberFieldValue(numberOfNewTracksField, numberOfNewTracks.toString()); //Set track type setTrackPopupOption('Track type', trackType); //Set track format if (trackFormat && trackType !== "Basic Folder" && trackType !== "VCA Master" && trackType !== "MIDI Track") { setTrackPopupOption('Track format', trackFormat); } //set track timebase if (trackTimebase && trackType !== "Basic Folder") { setTrackPopupOption('Track Timebase', trackTimebase); } //Set track Name if (trackName) { win.textFields.whoseTitle.is('Track Name').first.elementSetTextFieldWithAreaValue({ value: trackName, }); } if (clickCreate === true) { //Click Create win.buttons.whoseTitle.is('Create').first.elementClick(); //Wait for window to disappear win.elementWaitFor({ waitType: "Disappear" }); } } function importAudioFiles(directoryPath) { sf.ui.proTools.appActivateMainWindow(); sf.ui.proTools.menuClick({ menuPath: ["File", "Import", "Audio..."] }); const openWindow = sf.ui.proTools.windows.whoseTitle.is("Open").first; openWindow.elementWaitFor(); //Open the path field. sf.keyboard.press({ keys: "cmd+shift+g" }); const sheet = openWindow.sheets.first; sheet.elementWaitFor(); openWindow.sheets.first.textFields.first.elementSetTextAreaValue({ value: directoryPath, }); //Accept the directoryPath sf.keyboard.press({ keys: "return" }); sheet.elementWaitFor({ waitType: "Disappear" }); //Select all the audio files in directory. sf.ui.proTools.menuClick({ menuPath: ["Edit", "Select All"] }); //Add from "Clips in Current File" colum to "Clips to Import" column openWindow.buttons.whoseTitle.is("Add All ->").first.elementClick(); openWindow.buttons.whoseTitle.is("Open").first.elementClick(); openWindow.elementWaitFor({ waitType: "Disappear" }); const audioImportOptions = sf.ui.proTools.windows.whoseTitle.is("Audio Import Options").first; audioImportOptions.elementWaitFor(); audioImportOptions.radioButtons.whoseTitle.startsWith("Clip List").first.elementClick(); audioImportOptions.buttons.whoseTitle.is("OK").first.elementClick(); audioImportOptions.elementWaitFor({ waitType: "Disappear" }); } function getFilesInfo(directoryPath) { const selectedFilePaths = sf.file.directoryGetEntries({ path: directoryPath }).paths; let fileInfo = selectedFilePaths.map(path => ({ //fileName: path.split("/").pop(), //filePath: path, clipName: path.split("/").pop().split('.').slice(0, -1).join('.').replace(/(\.L)$|(\.R)$/, "").trim(), fileLength: sf.file.getFileInfo({ path }).fileInfo.length, })); //Create a unique set of file lenghts /** @ts-ignore */ const fileLengths = [...new Set(fileInfo.map(file => file.fileLength))]; let filesObject = {}; fileLengths.forEach(filelength => { let fileArray = fileInfo.filter(file => file.fileLength === filelength); let uniqueFileArray = fileArray.filter(({ clipName, fileLength }, index, a) => a.findIndex(e => clipName === e.clipName && fileLength === e.fileLength) === index, ); filesObject[filelength] = uniqueFileArray; }); return filesObject; } function getClipsInfo() { const clipList = sf.ui.proTools.mainWindow.clipListView; const rows = clipList.childrenByRole("AXRow"); const clipsInfo = rows.map(row => { const cell = row.children.whoseRole.is("AXCell"); const cellTextField = cell.allItems[1].children.whoseRole.is("AXStaticText").first; //const cellValue = cellTextField.value.invalidate().value; const cellTitle = cellTextField.title.invalidate().value; return { cell: cell.allItems[1], clipType: cellTitle.split("\n")[0], clipName: cellTitle.split(/\n\"/)[1].split(/\"/)[0], clipWidth: cellTitle.match(/\(([^()]+)\)/g)[0].replace("(samples)", ""), isSelected: cellTitle.startsWith("Selected. ") } }); return clipsInfo } function getClipWidth({ clipName }) { clipsInfo = getClipsInfo(); const clipWidth = clipsInfo.filter(clip => clip.clipName === clipName)[0].clipWidth.replace(/\(|\)/g, ""); const newClipWidth = clipWidth === "" ? "Mono" : clipWidth; return newClipWidth; } function selectClipsInClipList({ clipName }) { const clipsInfo = getClipsInfo(); const clipsToSelect = clipsInfo .filter(clip => clip.clipName === clipName && !clip.isSelected); const clipsToDeselect = clipsInfo .filter(clip => clip.clipName !== clipName) .filter(clip => clip.isSelected); //Select clips if they are not selected already clipsToSelect.forEach(clip => clip.cell.elementClick()); //Deselect other clips clipsToDeselect.forEach(clip => clip.cell.elementClick()); } function main() { //Activate and invalidate Pro Tools main window sf.ui.proTools.appActivateMainWindow(); sf.ui.proTools.mainWindow.invalidate(); const isClipListOpen = sf.ui.proTools.getMenuItem("View", "Other Displays", "Clip List").isMenuChecked; //Ensure the Clips List is visible. sf.ui.proTools.menuClick({ menuPath: ["View", "Other Displays", "Clip List"], targetValue: "Enable", }); //Ensure that the Clips List is not in Find mode ie. Click “Clear Find” in the Clips List Popup. sf.ui.proTools.mainWindow.popupButtons.whoseTitle.is('Clip List').first.popupMenuSelect({ menuPath: ["Clear Find"], }); sf.ui.proTools.appActivateMainWindow(); //Ensure The Clips List “Timeline Drop Order” is set to “Top-to-Bottom”. sf.ui.proTools.mainWindow.popupButtons.whoseTitle.is('Clip List').first.popupMenuSelect({ menuPath: ["Timeline Drop Order", "Top to Bottom"], targetValue: "Enable", }); //Prompt for the folder that holds the audio files. //const directoryPath = sf.interaction.selectFolder().path; const directoryPath = "/Users/kitchmembery/Desktop/Test Audio Files"; //From the selected directory, create an object of the audio files that contains information //on the files including the “File Name”, “File Length” and “Track Width”. const filesInfo = getFilesInfo(directoryPath); //Import all the audio files from the folder into Pro Tools via File => Import => Audio… importAudioFiles(directoryPath); //For each set of clips with matching length do the following: Object.keys(filesInfo).forEach(timeLength => { //For Each clip in the set => Object.values(filesInfo[timeLength]).forEach(clip => { //Get the track width const trackWidth = getClipWidth({ clipName: clip.clipName }); //Create a new track with matching track width. createNewTrack({ numberOfNewTracks: 1, trackFormat: trackWidth, trackType: "Audio Track", trackTimebase: "Samples", trackName: clip.clipName, clickCreate: true, }); //Select the clip in the Clips List. selectClipsInClipList({ clipName: clip.clipName }); //Spot the clip via “Spot to Edit Insertion” in the Clip List popup. sf.ui.proTools.mainWindow.popupButtons.whoseTitle.is('Clip List').first.popupMenuSelect({ menuPath: ["Spot to Edit Insertion"], }); }); }); //Restore Clip List State if (isClipListOpen === false) { sf.ui.proTools.menuClick({ menuPath: ["View", "Other Displays", "Clip List"], targetValue: "Enable", }); } //Alert when complete. sf.interaction.displayDialog({ prompt: "All the imported audio files have been spoted to new audio tracks", }); } main();
Let me know how it goes for you :-)
- Ddanielkassulke @danielkassulke
Wow - Kitch... this is absolutely spectacular. It has such huge significance for importing and sorting through takes of audio not locked to a grid. Thank you so much!
- Ddanielkassulke @danielkassulke
One closing thought that I suspect may benefit basically everyone who uses this - do you think it would be worthwhile introducing a clip-grouping function for each instance of audio files being chucked into the timeline?
Kitch Membery @Kitch2022-12-04 23:07:26.919Z
Hi @Daniel!
Glad you liked the script.
I thought about the clip grouping situation, it does add another layer of complexity to the workflow, as tracks of the same length could be if different track widths.
I'm not sure I'll have a chance to add this in the next few weeks but if it's something that you need I'll endeavor to do a refactor when I get a moment. :-)