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);
    
    
    
    • 5 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🤣

            1. AAdam Lilienfeldt @Adam_Lilienfeldt
                2026-01-13 12:34:05.685Z

                Hi @Chris_Shaw!

                I just realized I never replied to your excellent response on this post. Thanks for sharing your script! Great to get some input from a SF master like yourself.

                I've incorporated a bunch of your ideas and expanded upon mine into an updated version of my script:

                • Pagination: I've used your way of pagination the markers, while keeping my forward/prev buttons. It's running much smoother now!
                • Notification: I've added your persistent notification (very nice touch!)
                • Streamdeck detection: I've added your detection for which SD it's being triggered from, as well as the size calculation. I only have one 3x5 SD, so I haven't been able to test this, though.
                • Marker filtering: I've used your improved marker filtering to only get actual markers, and filter out track markers.
                • Loop selection: If Option is held while selecting a marker, it loops from that marker to the next one

                In the beginning of the script, there's some config options:

                • Exit the script after a selection
                • Auto-play on selection
                • Trigger a different script if run while holding Option (this is very specific to my workflow, feel free to ignore)

                Hope this can be useful!

                // ============ CONFIGURATION ============
                const CONFIG = {
                    // Auto-play after selecting a marker
                    autoPlay: true,
                    
                    // Exit the deck after selecting a marker (false = stay open for more selections)
                    exitAfterSelection: false,
                    
                    // Run custom command when holding Option key on script launch
                    optionKeyCommand: {
                        enabled: true,
                        commandId: "user:default:cm988inlo000jba105yh8s0ng"
                    }
                };
                // ============ Script Start ============
                sf.ui.useSfx();
                if (!sf.ui.proTools.isRunning) {
                    throw "Pro Tools is not running";
                }
                sf.ui.proTools.sfx.invalidate();
                // ============ Helper Functions ============
                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);
                }
                function paginateMarkers(markers, firstPageCapacity, otherPageCapacity) {
                    if (markers.length === 0) return [];
                    
                    const pages = [];
                    pages.push(markers.slice(0, firstPageCapacity));
                    
                    let remaining = markers.slice(firstPageCapacity);
                    while (remaining.length > 0) {
                        pages.push(remaining.slice(0, otherPageCapacity));
                        remaining = remaining.slice(otherPageCapacity);
                    }
                    
                    return pages;
                }
                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));
                }
                function selectBetween(no1, no2) {
                    sf.ui.proTools.sfx.memoryLocationsGoto({
                        memoryLocationNumber: no1,
                        restoreWindowOpenState: true,
                        useKeyboard: true,
                    });
                    sf.ui.proTools.sfx.memoryLocationsGoto({
                        extendSelection: true,
                        memoryLocationNumber: no2,
                        restoreWindowOpenState: true,
                        useKeyboard: true,
                    });
                    sf.ui.proTools.sfx.dawCommands.getByUniquePersistedName("LoopPlayback").run();
                    const isPtPlaying = sf.ui.proTools.sfx.isPlaying;
                    if (!isPtPlaying) {
                        sf.app.proTools.togglePlayState();
                    }
                }
                function getMarkerColor(markerName) {
                    const name = markerName.toLowerCase().replace(/\s+/g, "");
                    if (name.match(/c\d+/)) {
                        return { r: 255, g: 150, b: 0 };
                    }
                    if (name.match(/v\d+/)) {
                        return { r: 50, g: 200, b: 50 };
                    }
                    if (name.match(/pre\d+/)) {
                        return { r: 50, g: 150, b: 255 };
                    }
                    if (/\bstart\b/.test(markerName.toLowerCase()) || /\bout\b/.test(markerName.toLowerCase())) {
                        return { r: 180, g: 80, b: 255 };
                    }
                    return { r: 220, g: 50, b: 50 };
                }
                // ============ Main Function ============
                function showMarkerDeck() {
                    sf.ui.proTools.sfx.invalidate();
                    const sd = getStreamDeckDevice();
                    const { columnCount, rowCount } = sd;
                    const totalDeviceButtons = columnCount * rowCount;
                    const sortedMarkers = getSortedMarkerLocations();
                    const totalMarkers = sortedMarkers.length;
                    const singlePageCapacity = totalDeviceButtons - 1;
                    const firstPageCapacity = totalDeviceButtons - 2;
                    const otherPageCapacity = totalDeviceButtons - 3;
                    const needsPagination = totalMarkers > singlePageCapacity;
                    let markerPages;
                    if (!needsPagination) {
                        markerPages = totalMarkers > 0 ? [sortedMarkers] : [];
                    } else {
                        markerPages = paginateMarkers(sortedMarkers, firstPageCapacity, otherPageCapacity);
                    }
                    const totalPages = markerPages.length || 1;
                    const notificationId = sf.system.newGuid().guid;
                    sf.interaction.notify({
                        title: "Select Marker",
                        uid: notificationId,
                        keepAliveMs: 1000000
                    });
                    let currentPage = 0;
                    sd.doWithSeizedDevice({
                        action: () => {
                            while (true) {
                                const markersOnThisPage = markerPages[currentPage] || [];
                                const isLastPage = currentPage >= totalPages - 1;
                                const isFirstPage = currentPage === 0;
                                let allButtons = [
                                    {
                                        title: "←",
                                        color: { r: 0, g: 0, b: 0 },
                                        value: { type: "RETURN" }
                                    }
                                ];
                                const markerButtons = markersOnThisPage.map((marker, index) => {
                                    const globalIndex = sortedMarkers.indexOf(marker);
                                    const nextMarker = sortedMarkers[globalIndex + 1];
                                    return {
                                        title: marker.name,
                                        color: getMarkerColor(marker.name),
                                        value: {
                                            type: "MARKER",
                                            number: marker.number,
                                            name: marker.name,
                                            nextMarkerNumber: nextMarker ? nextMarker.number : undefined
                                        }
                                    };
                                });
                                allButtons = allButtons.concat(markerButtons);
                                let navSlotCount = 0;
                                if (needsPagination) {
                                    if (isFirstPage) {
                                        navSlotCount = 1;
                                    } else if (isLastPage) {
                                        navSlotCount = 2;
                                    } else {
                                        navSlotCount = 2;
                                    }
                                }
                                const emptySlots = totalDeviceButtons - allButtons.length - navSlotCount;
                                if (emptySlots > 0) {
                                    const emptyButtons = Array(emptySlots).fill({
                                        title: "",
                                        color: { r: 30, g: 30, b: 30 },
                                        value: { type: "EMPTY" }
                                    });
                                    allButtons = allButtons.concat(emptyButtons);
                                }
                                if (needsPagination) {
                                    if (isFirstPage) {
                                        allButtons.push({
                                            title: "▶",
                                            color: { r: 100, g: 100, b: 200 },
                                            value: { type: "NEXT_PAGE" }
                                        });
                                    } else if (isLastPage) {
                                        allButtons.push({
                                            title: "◀",
                                            color: { r: 100, g: 100, b: 200 },
                                            value: { type: "PREV_PAGE" }
                                        });
                                        allButtons.push({
                                            title: "",
                                            color: { r: 30, g: 30, b: 30 },
                                            value: { type: "EMPTY" }
                                        });
                                    } else {
                                        allButtons.push({
                                            title: "◀",
                                            color: { r: 100, g: 100, b: 200 },
                                            value: { type: "PREV_PAGE" }
                                        });
                                        allButtons.push({
                                            title: "▶",
                                            color: { r: 100, g: 100, b: 200 },
                                            value: { type: "NEXT_PAGE" }
                                        });
                                    }
                                }
                                var selectedButton = sd.showModal({ items: allButtons }).selectedItem;
                                var selection = selectedButton.value;
                                if (selection.type === "NEXT_PAGE" && !isLastPage) {
                                    currentPage++;
                                    continue;
                                }
                                if (selection.type === "PREV_PAGE" && !isFirstPage) {
                                    currentPage--;
                                    continue;
                                }
                                if (selection.type === "RETURN" || selection.type === "EMPTY") {
                                    break;
                                }
                                if (selection.type === "MARKER") {
                                    if (event.keyboardState.hasAlt && selection.nextMarkerNumber !== undefined) {
                                        selectBetween(selection.number, selection.nextMarkerNumber);
                                    } else {
                                        sf.ui.proTools.memoryLocationsGoto({ memoryLocationNumber: selection.number });
                                        // Auto-play based on config
                                        if (CONFIG.autoPlay) {
                                            const isPtPlaying = sf.ui.proTools.sfx.isPlaying;
                                            if (!isPtPlaying) {
                                                sf.app.proTools.togglePlayState();
                                            }
                                        }
                                    }
                                    
                                    // Exit or continue based on config
                                    if (CONFIG.exitAfterSelection) {
                                        break;
                                    }
                                    continue;
                                }
                            }
                            sf.interaction.notify({
                                uid: notificationId,
                                keepAliveMs: 1
                            });
                        }
                    });
                }
                // ============ Main Execution ============
                if (CONFIG.optionKeyCommand.enabled && event.keyboardState.hasAlt) {
                    log("Running custom command via Option key");
                    sf.soundflow.runCommand({
                        commandId: CONFIG.optionKeyCommand.commandId,
                    });
                } else {
                    showMarkerDeck();
                }