Show all memory locations on Streamdeck
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);
- 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 playingfunction 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!Chris Shaw @Chris_Shaw2025-05-02 21:18:47.823Z
Edit: Refactored with comments for clarity
- OOwen Granich-Young @Owen_Granich_Young
Love seeing these guys! Figured it was only a matter of time after that Ben Rubin Keypad Deck got made by you badasses!
Chris Shaw @Chris_Shaw2025-05-02 21:39:28.682Z
I was waiting to publish it when the locations SDK got more robust but this post forced my hand🤣