Spanner to Dolby Atmos Object Converter
By Sreejesh Nair @Sreejesh_Nair
I made a script as part of my Dolby Atmos Package as a utility where it will copy Spanner's automation into individual Object Auxes as pan automation. As some little features, I made it into a template and added fun things like identifying the spanner insert on the track by converting the insert number into the insert alphabet, intelligently calculating pan width from the user input by not including LFE in the object count, displaying automation lanes, programmatically reaching the adjacent tracks etc. Hope this script helps with some ideas as well.
// Extract track width from event properties
const { trWidth } = event.props;
// Calculate the number of pan automation channels based on track width
const panCount = calculatePanCount(trWidth);
// Function to calculate the number of channels from the track format
function calculatePanCount(format) {
return format.split('.').reduce((count, part) =>
count + (part !== "1" || format === "1" ? parseInt(part, 10) : 0), 0);
}
// Function to ensure all inserts (A-J) are displayed in the Pro Tools UI
let disFlags = [0, 0];
function ensureAllInsertsAreDisplayed() {
['Inserts A-E', 'Inserts F-J'].forEach((item, index) => {
if (!sf.ui.proTools.getMenuItem('View', 'Edit Window Views', item).isMenuChecked) {
sf.ui.proTools.menuClick({ menuPath: ["View", "Edit Window Views", item] });
disFlags[index] = 1;
}
});
}
// Function to find the insert slot (letter) of the Spanner plugin
function getSpannerInsertLetter() {
for (let i = 0; i < 10; i++) {
const pluginName = sf.ui.proTools.selectedTrack.insertButtons[i].value.invalidate().value;
if (pluginName === "Spanner") {
return String.fromCharCode(97 + i); // Convert insert number (0-9) to 'a'-'j'
}
}
return null; // Return null if Spanner is not found
}
// Function to select a specific automation lane of the Spanner plugin
function selectSpannerAutomationLane(laneName) {
const insertLetter = getSpannerInsertLetter();
if (!insertLetter) {
alert("Spanner plugin not found in the selected track.");
throw 0;
}
sf.ui.proTools.selectedTrack.trackDisplaySelect({
displayPath: [` (fx ${insertLetter}) Spanner`, laneName],
});
return true;
}
// Function to select a track relative to the currently selected track
function selectTrackDelta(delta) {
const selectedTrackName = sf.ui.proTools.selectedTrackNames[0];
const visibleTrackNames = sf.ui.proTools.visibleTrackNames;
const newTrackIndex = visibleTrackNames.findIndex(n => n === selectedTrackName) + delta;
if (newTrackIndex >= 0 && newTrackIndex < visibleTrackNames.length) {
sf.ui.proTools.trackSelectByName({ names: [visibleTrackNames[newTrackIndex]] });
}
}
// Function to determine which pan automation lane to paste into
function selectPanAutomationLaneForPasting(spannerLaneName) {
const laneMap = { "PanX_": ["front pos", "rear pos"], "PanY_": ["f/r pos"], "PanZ_": ["height"] };
let lanes = [];
for (const [key, value] of Object.entries(laneMap)) {
if (spannerLaneName.startsWith(key)) {
lanes = value;
break;
}
}
// Select the first lane in the array
if (lanes.length > 0) {
sf.ui.proTools.selectedTrack.trackDisplaySelect({ displayPath: ['pan', lanes[0]] });
}
return lanes;
}
// Function to perform a paste operation
function panPaste() {
sf.keyboard.press({ keys: "ctrl+cmd+v" });
}
// Function to check if there is a selection in Pro Tools
function checkSelectionLength() {
const selection = sf.ui.proTools.selectionGet();
const selectionLength = selection.selectionLength.split(":").slice(2).join(".");
return parseFloat(selectionLength) !== 0;
}
// Array of Spanner lane names to process
const spannerLaneNames = [
"PanX_L", "PanY_L", "PanZ_L", "PanX_C", "PanY_C", "PanZ_C",
"PanX_R", "PanY_R", "PanZ_R", "PanX_Lsr", "PanY_Lsr", "PanZ_Lsr",
"PanX_Rsr", "PanY_Rsr", "PanZ_Rsr", "PanX_Lss", "PanY_Lss", "PanZ_Lss",
"PanX_Rss", "PanY_Rss", "PanZ_Rss", "PanX_Lst", "PanY_Lst", "PanZ_Lst",
"PanX_Rst", "PanY_Rst", "PanZ_Rst", "PanX_Ltf", "PanY_Ltf", "PanZ_Ltf",
"PanX_Rtf", "PanY_Rtf", "PanZ_Rtf", "PanX_Ltr", "PanY_Ltr", "PanZ_Ltr",
"PanX_Rtr", "PanY_Rtr", "PanZ_Rtr", "PanX_Lw", "PanY_Lw", "PanZ_Lw",
"PanX_Rw", "PanY_Rw", "PanZ_Rw"
];
// Activate Pro Tools main window and ensure all inserts are displayed
sf.ui.proTools.appActivateMainWindow();
sf.ui.proTools.invalidate();
ensureAllInsertsAreDisplayed();
// Check for selection length before starting the process
if (!checkSelectionLength()) {
alert("There is no selection length in Pro Tools.");
throw 0;
} else {
selectTrackDelta(0); // Start with the Spanner track selected
let currentDelta = 1; // Initialize track increment
// Filter spannerLaneNames based on trWidth
const filteredSpannerLaneNames = trWidth === "2.0" || trWidth === "2" ?
spannerLaneNames.filter(name =>
["PanX_L", "PanY_L", "PanZ_L", "PanX_R", "PanY_R", "PanZ_R"].includes(name.split('_').slice(0, 2).join('_'))
) : spannerLaneNames;
for (let i = 0; i < filteredSpannerLaneNames.length; i++) {
// Halt process if currentDelta exceeds available tracks
if (currentDelta > panCount) {
break;
}
// Process each Spanner lane
if (selectSpannerAutomationLane(filteredSpannerLaneNames[i])) {
let trackIdentifier = filteredSpannerLaneNames[i].split('_').pop(); //To display the Pan Channel
sf.keyboard.press({ keys: "cmd+c" }); // Copy automation data
selectTrackDelta(currentDelta); // Move to target track for pasting
// Determine target pan lane(s) and paste automation data
const panLanes = selectPanAutomationLaneForPasting(filteredSpannerLaneNames[i]);
panLanes.forEach((lane, index) => {
sf.ui.proTools.selectedTrack.trackDisplaySelect({
displayPath: ['pan', lane],
});
if (index > 0) {
sf.wait({ intervalMs: 500 }); // Wait between lane selections
}
panPaste();
});
// Reset to Spanner track and increment delta after processing each set of three lanes
selectTrackDelta(-currentDelta);
if ((i + 1) % 3 === 0) {
log(`Pasted ${trackIdentifier} Channel`);
currentDelta++;
}
}
}
}
alert(`Completed Spanner conversion for ${trWidth}`);