No internet connection
  1. Home
  2. Script Sharing

Show all memory locations on Streamdeck

By Adam Lilienfeldt @Adam_Lilienfeldt
    2025-05-02 09:26:31.957Z

    Hi all,

    I thought i'd share this script i've been working on.

    It fetches all memory locations from Pro Tools and shows them on the streamdeck. If there are more than 14 memory locations, it will allow you to scroll between pages.
    Pressing a button will navigate to the corresponding location and start playback if it's not already playing.

    There's also some color coordination in there. It relates to my personal naming convention, but those can easily be changed:
    Markers called "C" followed by digits will be orange
    Markers called "V" followed by digits will be green
    Markers called "Pre" followed by digits will be blue
    Markers called "Start" og "Out" will be purple
    Everything else will be red.

    Please note that while the markers are being shown, it will block all other soundflow-commands from running. The script can be exited by pressing the top left button on the streamdeck.

    Have fun!

    var sd = sf.devices.streamDeck.firstDevice;
    
    if (!sf.ui.proTools.isRunning) {
        throw "Pro Tools is not running";
    }
    
    sf.ui.proTools.invalidate();
    
    var currentPage = 0; // Track which page of markers we're viewing
    
    function goToMemoryLocation(name, number) {
        // If this is our special button, perform the associated action
        if (name === "SKIP") {
            showMarkerDeck(currentPage, true); // Reload the current page
            return;
        } else if (name === "RETURN") {
            return;
        } else if (name === "NEXT_PAGE") {
            showMarkerDeck(currentPage + 1, true); // Go to next page and reload
            return;
        } else if (name === "PREV_PAGE") {
            showMarkerDeck(currentPage - 1, true); // Go to previous page and reload
            return;
        }
    
        // Go directly to the memory location using the number
        sf.ui.proTools.memoryLocationsGoto({ memoryLocationNumber: number });
    
        // IS PT playing
        const isPtPlaying = sf.ui.proTools.isPlaying;
        if (!isPtPlaying) {
            sf.app.proTools.togglePlayState();
        }
    
        // After navigating to the marker, force a deck refresh
        showMarkerDeck(currentPage, true); // Reload the current page
    }
    
    function getMarkerColor(markerName) {
        // Normalize: convert to lowercase and remove all spaces
        const name = markerName.toLowerCase().replace(/\s+/g, "");
    
        // Chorus markers: contains "c" followed by digits
        if (name.match(/c\d+/)) {
            return { r: 255, g: 150, b: 0 }; // Orange
        }
    
        // Verse markers: contains "v" followed by digits
        if (name.match(/v\d+/)) {
            return { r: 50, g: 200, b: 50 }; // Green
        }
    
        // Pre markers: contains "pre" followed by digits
        if (name.match(/pre\d+/)) {
            return { r: 50, g: 150, b: 255 }; // Blue
        }
    
        // Start or Out markers (match as full words only)
        if (/\bstart\b/.test(markerName.toLowerCase()) || /\bout\b/.test(markerName.toLowerCase())) {
            return { r: 180, g: 80, b: 255 }; // Purple
        }
    
        // Default color for all other markers
        return { r: 220, g: 50, b: 50 }; // Red
    }
    
    function showMarkerDeck(page = 0, forceReload = true) {
        // Only fetch memory locations if we need to reload
        if (forceReload) {
            sf.ui.proTools.invalidate();
    
            // Get markers using the new SDK method
            const memLocs = sf.app.proTools.memoryLocations.invalidate().allItems
                .map(memLoc => ({
                    number: memLoc.number,
                    name: memLoc.name,
                    startTime: memLoc.startTime,
                    endTime: memLoc.endTime
                }));
    
            //log(memLocs);
    
            // Sort markers by startTime
            const sortedMarkers = memLocs.sort((a, b) => Number(a.startTime) - Number(b.startTime));
    
            const memLocsArray = JSON.stringify(sortedMarkers, null, 2);
    
            const totalLocations = sortedMarkers.length;
    
            // Initialize a fixed-size array for all 15 buttons
            let markerItems = new Array(15).fill(null);
    
            // Set button 1 as RETURN
            markerItems[0] = {
                title: "← ", // Return button
                color: { r: 0, g: 0, b: 0 }, // Black color
                value: {
                    number: -2,
                    name: "RETURN"
                }
            };
    
            // Determine how many marker slots we have on this page
            let markerStartIndex, markerEndIndex;
            let maxMarkersOnCurrentPage;
    
            if (totalLocations <= 14) {
                // Case 1: All markers fit on one page - use buttons 2-15 for markers
                markerStartIndex = 0;
                markerEndIndex = totalLocations;
                maxMarkersOnCurrentPage = 14;
                page = 0; // Force to first page
                currentPage = 0;
            } else if (page === 0) {
                // Case 2: First page of multi-page case - use buttons 2-14 for markers, 15 for next
                markerStartIndex = 0;
                markerEndIndex = Math.min(13, totalLocations);
                maxMarkersOnCurrentPage = 13;
    
                // Add next page button at position 15
                markerItems[14] = {
                    title: "▶", // Right arrow for next page
                    color: { r: 100, g: 100, b: 200 }, // Purple
                    value: {
                        number: -5,
                        name: "NEXT_PAGE"
                    }
                };
            } else {
                // Case 3: Subsequent pages - use buttons 2-13 for markers, 14-15 for prev/next
                markerStartIndex = 13 + ((page - 1) * 12);
                markerEndIndex = Math.min(markerStartIndex + 12, totalLocations);
                maxMarkersOnCurrentPage = 12;
    
                // Calculate total pages
                const remainingAfterFirstPage = totalLocations - 13;
                const totalPages = 1 + Math.ceil(remainingAfterFirstPage / 12);
    
                // Ensure page is within valid range
                if (page >= totalPages) {
                    page = totalPages - 1;
                    currentPage = page;
                    // Recalculate indices
                    markerStartIndex = 13 + ((page - 1) * 12);
                    markerEndIndex = Math.min(markerStartIndex + 12, totalLocations);
                }
    
                // Add prev page button at position 14
                markerItems[13] = {
                    title: "◀", // Left arrow for previous page
                    color: { r: 100, g: 100, b: 200 }, // Purple
                    value: {
                        number: -4,
                        name: "PREV_PAGE"
                    }
                };
    
                // Add next page button at position 15 if not on last page
                if (page < totalPages - 1) {
                    markerItems[14] = {
                        title: "▶", // Right arrow for next page
                        color: { r: 100, g: 100, b: 200 }, // Purple
                        value: {
                            number: -5,
                            name: "NEXT_PAGE"
                        }
                    };
                } else {
                    markerItems[14] = {
                        title: "", // Empty if on last page
                        color: { r: 30, g: 30, b: 30 },
                        value: {
                            number: -1,
                            name: "SKIP"
                        }
                    };
                }
            }
    
            // Fill in marker slots
            for (let i = 0; i < maxMarkersOnCurrentPage; i++) {
                const markerIndex = markerStartIndex + i;
                const buttonIndex = i + 1; // +1 because button 1 is the return button
    
                if (markerIndex < totalLocations && markerIndex < markerEndIndex) {
                    // We have a marker for this position
                    const marker = sortedMarkers[markerIndex];
                    markerItems[buttonIndex] = {
                        title: marker.name,
                        color: getMarkerColor(marker.name),
                        value: {
                            number: marker.number,
                            name: marker.name
                        }
                    };
                } else {
                    // No marker for this position, add empty placeholder
                    markerItems[buttonIndex] = {
                        title: "",
                        color: { r: 30, g: 30, b: 30 },
                        value: {
                            number: -1,
                            name: "SKIP"
                        }
                    };
                }
            }
    
            // Ensure any remaining slots are filled with empty placeholders
            for (let i = 0; i < 15; i++) {
                if (!markerItems[i]) {
                    markerItems[i] = {
                        title: "",
                        color: { r: 30, g: 30, b: 30 },
                        value: {
                            number: -1,
                            name: "SKIP"
                        }
                    };
                }
            }
    
            // Update the current page
            currentPage = page;
    
            // Show the modal with all markers
            var selectedMarker = sd.showModal({
                items: markerItems
            }).selectedItem.value;
    
            // Process the selection
            goToMemoryLocation(selectedMarker.name, selectedMarker.number);
        }
    
        // Start the process by showing the first page of markers
    }
    
    
        showMarkerDeck(0, true);
    
    
    
    • 4 replies
    1. Chris Shaw @Chris_Shaw2025-05-02 19:07:23.636Z2025-05-03 15:47:54.485Z

      Hey @Adam_Lilienfeldt,
      This is quite good - I like your inclusion of a "previous" button.

      I wrote a similar script for myself a couple of weeks ago that has a similar function. The reason I'm sharing it here is that you might find a few things that you may want to incorporate to simplify your code.

      The key things here are:

      • the script can detect which SD device the script is being triggered from and the size / number of buttons of the SD being used (5x3 or 8x 4). This allows the script to automatically configure the number of buttons per SD page so the script can run on either size deck (Theres also a provision to choose a deck if the script is being triggered by the keyboard).
      • I use the chunkIntoPages function to break up the mem locations into an array of arrays without needing to use a for loop to build location pages/banks.
      • if ctrl is held when triggering the button it will recall the last selected location
      • a persistent notification window informs the user to pick a location.

      This script will revert to the previous deck once a location has been selected and allow other commands run.
      This script will not start playback when a location is selected but if PT is playing it will locate and continue playing

      function selectMemoryLocation() {
      
          // Recalls a memory location in Pro Tools by its number
          function recallMemoryLocation(locationNumber) {
              sf.app.proTools.selectMemoryLocation({ number: locationNumber });
          }
      
          // Splits an array into smaller arrays (pages) of specified size
          function chunkIntoPages(array, itemsPerPage) {
              const pages = [];
              for (let i = 0; i < array.length; i += itemsPerPage) {
                  pages.push(array.slice(i, i + itemsPerPage));
              }
              return pages;
          }
      
          // Determines the active Stream Deck device from the event context
          function getStreamDeckDevice() {
              
              const deckSerial = event.deck ? event.deck.instanceKey.split('.')[1] : null;
              return !deckSerial
                  ? sf.devices.streamDeck.invalidate().connectedDevices[0]
                  : sf.devices.streamDeck.invalidate().getDeviceBySerialNumber(deckSerial);
          }
      
          // Gets all Pro Tools memory locations marked as "Marker" and sorts them by time
          function getSortedMarkerLocations() {
              const allMemoryLocations = sf.app.proTools.memoryLocations.invalidate().allItems;
              return allMemoryLocations
                  .filter(location => location.timeProperties === "Marker")
                  .sort((a, b) => Number(a.startTimeInSamples) - Number(b.startTimeInSamples));
          }
      
          // Displays a notification on screen with the given title and duration
          function showNotification(title, uid, keepAliveMs) {
              sf.interaction.notify({
                  title: title,
                  uid: uid,
                  keepAliveMs: keepAliveMs
              });
          }
      
          // Generates display button configurations for Stream Deck
          function getDisplayButtons({
              locationsOnPage,
              totalDeviceButtons,
              RESERVED_BUTTONS,
              BLUE,
              GOLD,
              BLACK
          }) {
              const emptySlotsCount = totalDeviceButtons - locationsOnPage.length - RESERVED_BUTTONS.length;
              const emptyButtons = emptySlotsCount > 0 ? Array(emptySlotsCount).fill(' ') : [];
              const displayButtons = [...locationsOnPage, ...emptyButtons, ...RESERVED_BUTTONS];
      
              return displayButtons.map(button => {
                  let color;
                  if (typeof button === 'object') {
                      color = BLUE; // Markers are shown in blue
                  } else {
                      color = (button === ' ') ? BLACK : GOLD; // Reserved buttons are gold; empty are black
                  }
      
                  return {
                      title: typeof button === 'object' ? button.name : button,
                      value: typeof button === 'object' ? button.number : button,
                      color,
                  };
              });
          }
      
          // Quick recall if Control key is held and previous location exists
          const previousLocationNumber = globalState.prevLocationNumber;
          if (event.keyboardState.hasControl && previousLocationNumber) {
              return recallMemoryLocation(previousLocationNumber);
          }
      
          // Get current Stream Deck layout (rows and columns)
          const streamDeckDevice = getStreamDeckDevice();
          const { columnCount, rowCount } = streamDeckDevice;
          const totalDeviceButtons = columnCount * rowCount;
          const RESERVED_BUTTONS = ['Cancel', '-->']; // Buttons always shown on each page
      
          
          // Retrieve and paginate marker memory locations
          const markerLocations = getSortedMarkerLocations();
          const locationsByPage = chunkIntoPages(markerLocations, totalDeviceButtons - RESERVED_BUTTONS.length);
      
          // Define button colors
          const BLUE = { r: 0, g: 0, b: 100 };     // For memory markers
          const GOLD = { r: 119, g: 93, b: 32 };   // For control buttons (e.g., 'Cancel', '-->')
          const BLACK = { r: 0, g: 0, b: 0 };      // For empty filler buttons
      
          // Show persistent notification while selecting
          const notificationId = sf.system.newGuid().guid;
          showNotification("Select Memory Location", notificationId, 1000000);
      
          let currentPageIndex = 0;
      
          // Begin interaction with the Stream Deck
          streamDeckDevice.doWithSeizedDevice({
              action: () => {
                  while (true) {
                      const locationsOnPage = locationsByPage[currentPageIndex]; // Get current page
                      const items = getDisplayButtons({
                          locationsOnPage,
                          totalDeviceButtons,
                          RESERVED_BUTTONS,
                          BLUE,
                          GOLD,
                          BLACK
                      });
      
                      // Show buttons and wait for user input
                      var selectedButton = streamDeckDevice.showModal({ items }).selectedItem;
      
                      // Handle user input
                      if (selectedButton.title === "Cancel") {
                          break; // Exit loop without action
                      }
      
                      if (selectedButton.title === "-->") {
                          currentPageIndex = (currentPageIndex + 1) % locationsByPage.length; // Next page
                          continue; // Refresh buttons
                      }
      
                      break; // Selection made
                  }
      
                  // If valid memory location selected, store and recall it
                  if (typeof selectedButton.value === 'number') {
                      globalState.prevLocationNumber = selectedButton.value;
                      recallMemoryLocation(selectedButton.value);
                  }
      
                  // Show brief "Locating" notification
                  showNotification("Locating", notificationId, 100);
              }
          });
      }
      
      // Execute the main function
      selectMemoryLocation()
      
      

      I have a version that colors the buttons depending on the location's name but it's a bit convoluted so I left it out of this script.
      Use what you want (or not)
      Great work!

      1. Edit: Refactored with comments for clarity

        1. Love seeing these guys! Figured it was only a matter of time after that Ben Rubin Keypad Deck got made by you badasses!

          1. I was waiting to publish it when the locations SDK got more robust but this post forced my hand🤣