No internet connection
  1. Home
  2. Ideas
  3. PT SDK Ideas

Cascade Audio In Mastering Application

By Nathan Salefski @nathansalefski
    2024-02-14 16:07:53.393Z

    Prepping mastering sessions in Pro Tools is quite frankly, annoying. After seeing the potential in the SDK from using it yesterday, I was wondering why this functionality isn't already built in. The screen recording makes it pretty apparent what it's doing but essentially it's allowing each song to be proceeded by the next song with markers. This makes printing masters incredibly easy and navigating the mastering session equally so. I was imagining it would look something like this:

    sf.app.proTools.cascadeTracks({
        numberTracks: true, // Adds numbering to track names
        gaplessTransitions: false,
        startPadding: 80ms,
        endPadding: 120ms,
        startMarker: trackName, // Accounts for padding
        endMarker: clipName, // Accounts for padding
    });
    
    Below is the script I wrote and the screen recording, let me know your thoughts!
    function returnToZero() {
        const trasnport = sf.ui.proTools.mainWindow.transportViewCluster.groups.whoseTitle.is("Normal Transport Buttons");
    
        trasnport.first.buttons.whoseTitle.is("Return to Zero").first.elementClick();
    }
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////
    
    function getOriginalCounter() {
        let originalCounter;
    
        if (sf.ui.proTools.getMenuItem('View', 'Main Counter', 'Bars|Beats').isMenuChecked) {
            originalCounter = ['View', 'Main Counter', 'Bars|Beats'];
        }
    
        if (sf.ui.proTools.getMenuItem('View', 'Main Counter', 'Min:Secs').isMenuChecked) {
            originalCounter = ['View', 'Main Counter', 'Min:Secs'];
        }
    
        if (sf.ui.proTools.getMenuItem('View', 'Main Counter', 'Timecode').isMenuChecked) {
            originalCounter = ['View', 'Main Counter', 'Timecode'];
        }
    
        if (sf.ui.proTools.getMenuItem('View', 'Main Counter', 'Feet+Frames').isMenuChecked) {
            originalCounter = ['View', 'Main Counter', 'Feet+Frames'];
        }
    
        if (sf.ui.proTools.getMenuItem('View', 'Main Counter', 'Samples').isMenuChecked) {
            originalCounter = ['View', 'Main Counter', 'Samples'];
        }
    
        return originalCounter;
    }
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////
    
    function getOriginalEditMode() {
        let editMode;
    
        const slipModeButton = sf.ui.proTools.mainWindow.editModeCluster.buttons.whoseTitle.is("EditModeSlip").first;
        const gridModeButton = sf.ui.proTools.mainWindow.editModeCluster.buttons.whoseTitle.is("Absolute Grid mode").first;
        const shuffleModeButton = sf.ui.proTools.mainWindow.editModeCluster.buttons.whoseTitle.is("EditModeShuffle").first;
        const spotModeButton = sf.ui.proTools.mainWindow.editModeCluster.buttons.whoseTitle.is("EditModeSpot").first;
    
        if (slipModeButton.value.invalidate().value === 'Selected') {
            editMode = 'Slip';
        }
    
        if (gridModeButton.value.invalidate().value === 'Selected') {
            editMode = 'Grid';
        }
    
        if (shuffleModeButton.value.invalidate().value === 'Selected') {
            editMode = 'Shuffle';
        }
    
        if (spotModeButton.value.invalidate().value === 'Selected') {
            editMode = 'Spot';
        }
    
        return editMode;
    }
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////
    
    function batchRename() {
        sf.ui.proTools.selectedTrack.titleButton.popupMenuSelect({
            isRightClick: true,
            relativePosition: { x: 5, y: 5 },
            menuPath: ['Batch Rename...']
        });
    
        const batchRenameWin = sf.ui.proTools.windows.whoseTitle.is('Batch Track Rename').first;
    
        batchRenameWin.elementWaitFor();
    
        batchRenameWin.checkBoxes.whoseTitle.is('Replace').first.checkboxSet({
            targetValue: 'Disable',
        });
    
        batchRenameWin.checkBoxes.whoseTitle.is('Trim').first.checkboxSet({
            targetValue: 'Disable',
        });
    
        batchRenameWin.checkBoxes.whoseTitle.is('Add').first.checkboxSet({
            targetValue: 'Disable',
        });
    
        batchRenameWin.checkBoxes.whoseTitle.is('Numbering').first.checkboxSet({
            targetValue: 'Enable',
        });
    
        batchRenameWin.popupButtons.first.popupMenuSelect({
            menuPath: ['Beginning'],
        });
    
        // Starting Number
        batchRenameWin.textFields.allItems[11].elementSetTextFieldWithAreaValue({
            useMouseKeyboard: true,
            value: '1'
        });
    
        // Number of Places
        batchRenameWin.textFields.allItems[12].elementSetTextFieldWithAreaValue({
            useMouseKeyboard: true,
            value: '1'
        });
    
        // Increment
        batchRenameWin.textFields.allItems[13].elementSetTextFieldWithAreaValue({
            useMouseKeyboard: true,
            value: '1'
        });
    
        batchRenameWin.checkBoxes.whoseTitle.is('Separate With:').first.checkboxSet({
            targetValue: 'Enable',
        });
    
        batchRenameWin.buttons.whoseTitle.is('OK').first.elementClick();
    
    }
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////
    
    function randomTrackColor() {
        const color = ((Math.random() * 22) + 1).toFixed(0);
    
        const brightnessList = ['Light', 'Medium', 'Dark'];
    
        const brightness = brightnessList[(Math.random() * 2).toFixed(0)];
    
        sf.ui.proTools.colorsSelect({
            colorTarget: 'Tracks',
            colorNumber: Number(color),
            colorBrightness: brightness
        });
    }
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////
    
    function spotClip(start) {
        const spotModeButton = sf.ui.proTools.mainWindow.editModeCluster.buttons.whoseTitle.is("EditModeSpot").first;
    
        if (!spotModeButton.isFocused) {
            sf.ui.proTools.editModeSet({
                mode: 'Spot'
            });
        }
    
        returnToZero();
    
        // Move Edit Selection to Next Clip
        sf.keyboard.press({
            keys: 'control+tab'
        });
    
        // Zoom to Fit Horizontal Selection
        sf.keyboard.press({
            keys: 'control+alt+f'
        });
    
        sf.ui.proTools.toolsSelect({
            tool: "Grabber",
        });
    
        const selectedTrack = sf.ui.proTools.selectedTrack.frame;
    
        sf.mouse.setPosition({
            position: { x: selectedTrack.x + 600, y: selectedTrack.y + (selectedTrack.h / 2) }
        });
    
        sf.mouse.click({
            position: { x: selectedTrack.x + 600, y: selectedTrack.y + (selectedTrack.h / 2) }
        });
    
        const spotDialog = sf.ui.proTools.clipSpotDialog;
    
        spotDialog.elementWaitFor();
    
        spotDialog.textFields.first.elementSetTextFieldWithAreaValue({
            useMouseKeyboard: true,
            value: `${start}`
        });
    
        spotDialog.buttons.whoseTitle.is('OK').first.elementClick();
    }
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////
    
    function createMarker(selectedTrackName) {
    
        const match = selectedTrackName.match(/^(\d+)[\s_](.+)$/);
    
        const markerName = match[2];
    
        const markerNumber = Number(match[1]);
    
        sf.ui.proTools.memoryLocationsCreate({
            name: markerName,
            memoryLocationNumber: markerNumber,
        });
    }
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////
    
    function getPreviousTrackEnd() {
        returnToZero();
    
        sf.ui.proTools.menuClick({
            menuPath: ['Edit', 'Selection', 'Extend Edit Up']
        });
    
        sf.ui.proTools.menuClick({
            menuPath: ['Edit', 'Selection', 'Remove Edit from Bottom']
        });
    
        // Move Edit Selection to Next Clip
        sf.keyboard.press({
            keys: 'control+tab'
        });
    
        const previousTrackEnd = sf.ui.proTools.selectionGetInSamples().selectionEnd;
    
        return previousTrackEnd
    }
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////
    
    function restore(originalCounter, editMode) {
        sf.ui.proTools.menuClick({
            menuPath: ['Edit', 'Select All']
        });
    
        // Zoom to Fit Horizontal Selection
        sf.keyboard.press({
            keys: 'control+alt+f'
        });
    
        sf.ui.proTools.trackDeselectAll();
    
        returnToZero();
    
        sf.ui.proTools.toolsSelect({
            tool: "Smart",
        });
    
        sf.ui.proTools.menuClick({
            menuPath: originalCounter
        });
    
        sf.ui.proTools.editModeSet({
            mode: editMode
        });
    }
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////
    
    function main() {
        sf.ui.proTools.appActivateMainWindow();
        sf.ui.proTools.mainWindow.invalidate();
    
        const originalCounter = getOriginalCounter();
    
        const editMode = getOriginalEditMode();
    
        if (!sf.ui.proTools.getMenuItem('View', 'Main Counter', 'Samples').isMenuChecked) {
            sf.ui.proTools.menuClick({
                menuPath: ['View', 'Main Counter', 'Samples']
            });
        }
    
        batchRename();
    
        sf.ui.proTools.selectedTrackNames.forEach((trackName, trackNameIndex) => {
    
            sf.ui.proTools.trackSelectByName({ names: [trackName]});
    
            sf.ui.proTools.selectedTrack.trackScrollToView();
    
            randomTrackColor();
    
            // IF FIRST TRACK JUST CREATE MARKER
            if (trackNameIndex === 0) {
                createMarker(trackName);
                return
            } else {
                let previousTrackEnd = getPreviousTrackEnd();
    
                sf.ui.proTools.trackSelectByName({
                    names: [trackName]
                });
    
                spotClip(previousTrackEnd);
    
                createMarker(trackName);
    
                returnToZero();
            }
        });
    
        restore(originalCounter, editMode);
    }
    
    main();
    
    • 16 replies

    There are 16 replies. Estimated reading time: 11 minutes

    1. In reply tonathansalefski:
      Nathan Salefski @nathansalefski
        2024-02-14 16:27:34.914Z

        No related to the SDK per se but an entire new Mastering Track Type, with assignable pre and post fader inserts would be amazing for mastering. It would also allow the clip on the track to be used as the reference by some sort of toggle

        1. In reply tonathansalefski:
          Dustin Harris @Dustin_Harris
            2024-02-16 15:34:16.560Z

            I haven't tested your code, but you're closer than you think to having the example you listed at the top. If you pass those parameters as an object (curly braces) with default assignments, you can then use the parameters provided, like this:

            function nathanSafalskiCascadeTracks({
                numberTracks = true,
                gaplessTransitions = false,
                startPaddingInMs = 0,
                endPaddingInMs = 0,
                startMarker = 'trackName', // Accounts for padding
                endMarker = 'clipName', // Accounts for padding
            }) {
            /* the rest of you main() function code goes here, but you use the input parameters to decide how the script behaves */
            
                //example
                if (numberTracks == true) {
                    batchRename();
                }
            
                //another example
                if (startMarker == "trackName") {
                    createMarker(trackName);
                } else if (startMarker == "somethingElse") {
                    createMarker(somethingElse)
                }
            }
            

            and then when you call the function, the parameters pop up, just like the SoundFlow ones:

            1. Dustin Harris @Dustin_Harris
                2024-02-16 15:44:47.039Z

                you would need to add some code to convert the ms values to samples:

                /**@param {number} ms - duration in milliseconds */
                function msToSamples(ms) {
                    const sampleRate = Number(sf.app.proTools.getSessionSampleRate().sampleRate.replace("SR", ""))
                    
                    //round to the closest sample
                    return Math.round(ms/1000 * sampleRate)
                }
                
                const samplesToPad = msToSamples(117)
                log(samplesToPad)
                
                1. In reply toDustin_Harris:
                  Nathan Salefski @nathansalefski
                    2024-02-16 15:52:05.326Z

                    Oh that makes a lot of sense. I remember @Chad mentioned this to me before but it's just now clicking. That's awesome! That would be really helpful because I was considering switching to WaveLab for this functionality. This might even work a little more smoothly if converted into a template. Thanks man!

                    1. Dustin Harris @Dustin_Harris
                        2024-02-16 15:55:15.509Z

                        and then you can use the gaplessTransitions parameter to decide if the padding is necessary, like this:

                        const padInSamples = msToSamples(startPaddingInMs)
                        
                        //ignore pad if gaplessTransitions is set to true
                        const nextSongStartInSamples = (gaplessTransitions == true) ? previousTrackEnd : previousTrackEnd + padInSamples
                        
                        spotClip(nextSongStartInSamples)
                        
                        1. Nathan Salefski @nathansalefski
                            2024-02-16 16:01:27.353Z

                            Incredible. I don't exactly know the what this is called where you use a ?. Like a conditional variable const nextSongStartInSamples = (gaplessTransitions == true) ? previousTrackEnd : previousTrackEnd + padInSamples, but that seems very handy in other instances

                            1. Dustin Harris @Dustin_Harris
                                2024-02-16 16:05:42.910Z

                                that's a code shortcut called a ternary operator:

                                const nextSongStartInSamples = (gaplessTransitions == true) ? previousTrackEnd : previousTrackEnd + padInSamples
                                

                                is the same as this:

                                let nextSongStartInSamples = previousTrackEnd;
                                if (gaplessTransitions == false) {
                                    nextSongStartInSamples = previousTrackEnd + padInSamples
                                }
                                
                              • In reply toDustin_Harris:
                                Dustin Harris @Dustin_Harris
                                  2024-02-16 16:02:43.114Z

                                  and just for the sake of fun and learning, you can simplify your getOriginalEditMode and restore functions like this:

                                  function getOriginalEditMode() {
                                      const modeButtons = sf.ui.proTools.mainWindow.editModeCluster.buttons
                                      const selectedButton = modeButtons.filter(btn => btn.value.invalidate().value == "Selected")[0]
                                      return selectedButton.title.value
                                  }
                                  
                                  const originalEditMode = getOriginalEditMode();
                                  
                                  function restoreEditMode(modeButtonTitle) {
                                      sf.ui.proTools.mainWindow.editModeCluster.buttons.whoseTitle.is(modeButtonTitle).first.elementClick();
                                  }
                                  

                                  array methods like .filter and .map are really powerful and will kick your code up a notch, and make it faster to write :)

                                  1. Dustin Harris @Dustin_Harris
                                      2024-02-16 16:16:19.050Z

                                      and to make the default assignments clearer:

                                      function thisIsHowDefaultAssignmentsWork({ firstName = "Dustin", lastName = "Harris" }) {
                                      
                                          log(firstName + " " + lastName)
                                      
                                      }
                                      
                                      //returns "Dustin Harris"
                                      thisIsHowDefaultAssignmentsWork({})
                                      
                                      //returns "Nathan Salefski"
                                      thisIsHowDefaultAssignmentsWork({firstName: "Nathan", lastName: "Salefski"})
                                      
                                      //returns "Dustin Salefski"
                                      thisIsHowDefaultAssignmentsWork({lastName: "Salefski"})
                                      
                                      1. Dustin Harris @Dustin_Harris
                                          2024-02-16 16:21:09.862Z

                                          AND just because you'll see it elsewhere on the forum:

                                          //this
                                          if (gaplessTransitions) {
                                              //..doSomething
                                          }
                                          
                                          //is the same as this
                                          if (gaplessTransitions == true) {
                                              //..doSomething
                                          }
                                          
                                          //and this
                                          if (!gaplessTransitions) {
                                              //..doAnotherThing
                                          }
                                          
                                          //is the same as
                                          
                                          if (gaplessTransitions == false) {
                                              //..doAnotherThing
                                          }
                                          
                                          1. Nathan Salefski @nathansalefski
                                              2024-02-16 17:32:36.228Z

                                              I do at least know those lol. Than you man. I really appreciate the information because I'm constantly learning. I have absolutely no coding background but programming SoundFlow has been incredibly fun and challenging. I really appreciate it!

                                              1. Dustin Harris @Dustin_Harris
                                                  2024-02-16 17:40:47.098Z

                                                  I started the exact same way :) Just keep writing scripts, reading the forum, and asking questions and osmosis will do its thing! There are awesome free JavaScript tutorials on the web too

                                      2. In reply toDustin_Harris:
                                        Nathan Salefski @nathansalefski
                                          2024-03-08 20:08:27.395Z

                                          How can I make an each parameter have some predefined options to choose from? For example when you do something like .checkboxSet() you have targetValue as a parameter but you can then see options for targetValue like "Enable" and "Disable"

                                          1. Dustin Harris @Dustin_Harris
                                              2024-03-08 21:38:08.865Z

                                              do you that by documenting the parameters with something called JSDOC, like this:

                                              /**
                                               * This function demonstrates how default assignments work in JavaScript.
                                               * If no parameters are provided, it uses default values for firstName and lastName.
                                               * @param {Object} options - An object containing optional parameters.
                                               * @param {"Dustin" | "Nathan"} [options.firstName="Dustin"] - The first name. Defaults to "Dustin" if not provided.
                                               * @param {"Harris" | "Salefski"} [options.lastName="Harris"] - The last name. Defaults to "Harris" if not provided.
                                               */
                                              function thisIsHowDefaultAssignmentsWork({ firstName = "Dustin", lastName = "Harris" }) {
                                                  log(firstName + " " + lastName);
                                              }
                                              
                                              thisIsHowDefaultAssignmentsWork({})
                                              

                                              You can see from the code above that I've limited the firstName and lastName to be my name OR your name (using the "|" character), so now they are the only two suggestions:

                                              1. Nathan Salefski @nathansalefski
                                                  2024-03-08 22:38:50.070Z2024-03-08 23:04:12.353Z

                                                  Amazing thank you!