No internet connection
  1. Home
  2. How to

Arrange Clips By Length In Timeline

By danielkassulke @danielkassulke
    2022-11-19 08:43:40.563Z2022-11-20 02:52:32.975Z

    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"],
    
    Solved in post #5, click to view
    • 7 replies
    1. Kitch Membery @Kitch2022-11-23 03:30:18.362Z

      Hi Daniel,

      I'll take a look at this when I get a chance. :-)

      1. 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.
        • Repeat for each set of clips with matching lengths.

        • Alert when complete.

        Let me know :-)

        1. D
          In reply todanielkassulke:
          danielkassulke @danielkassulke
            2022-11-28 22:25:15.493Z

            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

            1. 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 :-)

              ReplySolution
              1. Ddanielkassulke @danielkassulke
                  2022-12-03 03:07:03.850Z

                  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!

                  1. Ddanielkassulke @danielkassulke
                      2022-12-03 03:23:24.595Z

                      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?

                      1. 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. :-)