No internet connection
  1. Home
  2. How to

Modifier key + Stream Deck combo implementation

By Sam Ada @Sam_Ada
    2022-12-14 23:58:26.328Z

    Hi all!

    Just started with soundflow on Sunday and am super excited to improve my workflows.

    I'm looking at creating some workflows which allow me to get quick access to my send volume automation data and display plugins on corresponding sends.

    I have most of the scripts to do what I need functioning separately eg. I have these 2 scripts below and will add others to it over time things like 'show mute' and more.

    Script one

    sf.ui.proTools.selectedTrack.displaySelectorButton.popupMenuSelect({
        isShift: true,
        isOption: true,
        menuPath: [`*(snd a)*`, "level"],
        useWildcards: true
    });
    
    

    Script two

    
    sf.ui.proTools.selectedTrack.displaySelectorButton.popupMenuSelect({
        menuPath: ["*(snd a)*","level"],
        isOption: true,
        useWildcards: true,
    });
    

    My desired workflow is when no modifiers are selected and I hit stream deck button it shows corresponding sends level ie script one above. When I hit 'option' modifier and press same stream deck button it shows corresponding sends level on all tracks ie script two.

    My question is what is the best way to implement such feature so that I don't back myself into a corner in expanding this later on?

    I have seen three implementations to do something like this, but I guess could be more'

    1. Kitch's video on YouTube regarding if/else statements which I guess would be self contained scripts per button.
    2. Nick Leyer's AWESOME package for izotope RX and he has his scripts broken up into small scripts and has a script that points to them(although how they are pointing to them I have no clue..... yet).
    3. Owen Granich Young's Multi-Function buttons for dummys package implementation which uses 'command ID' to point to commands

    I will say my level of coding experience is super basic but previous when doing Arduino stuff I was able to work stuff out so please bear with me if I ask lots of Q's

    Thanks for any help I hope I was able to describe what i'm trying to do.

    • 4 replies
    1. this should get you started:

      // Get modifier keys (if any) from trigger event - returned as a string
      const modifiers = event.keyboardState.asString
      
      function determineTopMostEditTrack() {
          sf.ui.proTools.mainWindow.invalidate();
          const editTimeLineTopY = sf.ui.proTools.mainWindow.timelineFocusButton.frame.y;
          const topTrack = sf.ui.proTools.visibleTrackHeaders.filter(h => h.frame.y >= editTimeLineTopY)[0];
          const topTrackName = topTrack.normalizedTrackName;
          return topTrackName;
      };
      
      // Switch to PT
      sf.ui.proTools.appActivateMainWindow()
      
      // get selected track(s)
      let selectedTracks = sf.ui.proTools.selectedTrackNames
      
      //If no track is selected select top edit track
      if (selectedTracks.length == 0) {
          sf.ui.proTools.trackSelectByName({
              names: [determineTopMostEditTrack()]
          })
      }
      
      // scroll to selected track(s)
      sf.ui.proTools.selectedTrack.trackScrollToView();
      
      // if no modifiers are held set view on selected track(s) to send a
      if (modifiers == "") {
          sf.ui.proTools.selectedTrack.displaySelectorButton.popupMenuSelect({
              menuPath: ["*(snd a)*", "level"],
              isShift: true,
              isOption: true,
              useWildcards: true
          });
      }
      
      // if ctrl (alt) is held set view for all tracks to send a level 
      if (modifiers == "alt") {
          sf.ui.proTools.selectedTrack.displaySelectorButton.popupMenuSelect({
              menuPath: ["*(snd a)*", "level"],
              isShift: false,
              isOption: true,
              useWildcards: true,
          });
      }
      
      // if ctrl + opt (alt) + cmd is held set view for all tracks to waveform
      if (modifiers == "ctrl+alt+cmd") {
          sf.ui.proTools.selectedTrack.displaySelectorButton.popupMenuSelect({
              menuPath: ["waveform"],
              isShift: false,
              isOption: true,
              useWildcards: true,
          });
      }
      
      
      1. SSam Ada @Sam_Ada
          2022-12-17 14:01:05.574Z

          Thanks Chris! I take it you recommend the all in one script approach. Can I ask if you have a good resource that I can learn what the functions you are using here do? The main part that I don’t understand is the functions you are doing before the if statements.

          1. Chris Shaw @Chris_Shaw2022-12-17 23:19:54.903Z2022-12-18 00:38:45.057Z
            // Get modifier keys (if any) from trigger event - returned as a string
            const modifiers = event.keyboardState.asString
            

            Every time a script is triggered there is an event which returns a number of things - which keyboard modifiers are being held, arguments and properties from another script that triggered the current script, and other things that I wont get into.
            With the above two lines we're getting which modifiers were held when the script was triggered. Normally you can check for hasControl,hasAlt, hasCommand, or hasShift but to be concise we're getting asString which will return any modifiers being held as a string. The order they are returned in a string is ctrl+alt+shift+cmd

            function determineTopMostEditTrack() {
                sf.ui.proTools.mainWindow.invalidate();
                const editTimeLineTopY = sf.ui.proTools.mainWindow.timelineFocusButton.frame.y;
                const topTrack = sf.ui.proTools.visibleTrackHeaders.filter(h => h.frame.y >= editTimeLineTopY)[0];
                const topTrackName = topTrack.normalizedTrackName;
                return topTrackName;
            };
            

            This function determines which is the top most track visible in the edit window just below the rulers in PT.
            To be clear, we're defining the function and what it does - we're not running / calling it. That will happen later on in the script.


            • Line #2 (sf.ui.proTools.mainWindow.invalidate();
            SF keeps a cache of all of the GUI elements in the PT edit window to speed up its response. It updates itself in the background but if you , for instance, add a new track in the session then its cache becomes out of date. (ex. - If you add a new track between the first and second track in the session then tell SF to select the third track SF may select the new track you've just created). Because of this it's always a good idea to refresh or invalidate() the main window which forces SF to update its cache. @chrscheuer has a very good post regarding invalidating the cache here - When to use invalidate(). Generally speaking, when SF seems to behave in a weird way when selecting things or is returning unexpected values when getting data from the PT GUI then you probably need to use invalidate() somewhere in your script.


            • Line 3 (const editTimeLineTopY = sf.ui.proTools.mainWindow.timelineFocusButton.frame.y;)
            Every GUI element (buttons, labels, windows, etc) has a frame that encapsulates it.
            Here the script is getting the y pixel coordinate of the timeline a-z focus button (it's the top left corner y coordinate which is the same as the top y coordinate of the tracks view in the edit window ) and stores it in the constant editTimeLineTopY:

            With this info we can determine which track is directly below it…


            •Line 4 ( const topTrack = sf.ui.proTools.visibleTrackHeaders.filter(h => h.frame.y >= editTimeLineTopY)[0];)
            This line (sf.ui.proTools.visibleTrackHeaders ) gets all of the track headers in the edit window - essentially all of the available GUI info for each visible track. The .filter method iterates through each header, and puts all of the tracks whose frame has a y value greater than the y value of the az button into an array and returns the first one [0] (In JavaScript the first item of an array is numbered 0, the second item is 1, etc). This first header is the first visible track below the az button. This header is stored in the topTrack constant.


            •Line 5 (const topTrackName = topTrack.normalizedTrackName;)
            Now that we've got the header for the top visible track this line grabs the name of the track from the header. We use normalizedTrackName because the track name in the header has more info than is displayed in the PT track name. Usually it returns a track name along with the track type: (ex. "Snare - Audio Track).


            •Line 6 (return topTrackName)

            This line returns then constant topTrackName from the function:
            Normally, I would put a line like this near the top of the script:

            const topVisibleEditWInTrack = determineTopMostEditTrack()
            

            to store the name of the top most edit window track. However in this script we only need to get the top visible track if no tracks are selected when the script is run. So instead of having it called every time this script is run it's only called when no track is selected and it's called in the name parameter in this part of the script:

            //If no track is selected select top edit track
            if (selectedTracks.length == 0) {
                sf.ui.proTools.trackSelectByName({
                    names: [determineTopMostEditTrack()] /* <== here is where the function is called
                })
            }
            

            Hopefully this sheds some light on your question.

            1. SSam Ada @Sam_Ada
                2022-12-19 16:45:53.825Z

                Again! thank you Chris! i'm excited to delve into in more when I have more spare time! hopefully I can get to a spot where I can just get done all I want and need without all the Q's