No internet connection
  1. Home
  2. Script Sharing

Spanner to Dolby Atmos Object Converter

By Sreejesh Nair @Sreejesh_Nair
    2023-11-15 19:17:37.002Z2025-06-12 07:28:02.891Z

    Update: It automatically detects the width of the track spanner is inserted in and pastes the pan accordingly. You no longer need to use it as a template.

    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 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.

    // Unique notification ID
    const notificationID = sf.system.newGuid().guid;
    
    // Get selected track's format
    sf.app.proTools.invalidate();
    const selectedTrack = sf.app.proTools.tracks.allItems.find(t => t.isSelected);
    if (!selectedTrack) throw `No track is selected.`;
    
    // Format map
    const formatMapping = {
        "TFMono": "1.0", "TFStereo": "2.0", "TFLCR": "3.0", "TFQuad": "4.0",
        "TF50": "5.0", "TF51": "5.1", "TF502": "5.0.2", "TF512": "5.1.2",
        "TF504": "5.0.4", "TF514": "5.1.4", "TF70": "7.0", "TF71": "7.1",
        "TF702": "7.0.2", "TF712": "7.1.2", "TF704": "7.0.4", "TF714": "7.1.4",
        "TF706": "7.0.6", "TF716": "7.1.6", "TF904": "9.0.4", "TF914": "9.1.4",
        "TF906": "9.0.6", "TF916": "9.1.6"
    };
    
    const trFormatRaw = selectedTrack.format;
    const trWidth = formatMapping[trFormatRaw];
    if (!trWidth) throw `Unrecognized track format: ${trFormatRaw}`;
    
    // Calculate pan count (excluding LFE unless mono)
    function calculatePanCount(format) {
        return format.split('.').reduce((count, part) =>
            count + (part !== "1" || format === "1" ? parseInt(part, 10) : 0), 0);
    }
    const panCount = calculatePanCount(trWidth);
    
    // Lane list
    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"
    ];
    const filteredSpannerLaneNames = spannerLaneNames.slice(0, panCount * 3);
    
    // Helper: Show inserts
    function ensureAllInsertsAreDisplayed() {
        ["Inserts A-E", "Inserts F-J"].forEach(item => {
            const menuItem = sf.ui.proTools.getMenuItem("View", "Edit Window Views", item);
            if (!menuItem.isMenuChecked) {
                sf.ui.proTools.menuClick({ menuPath: ["View", "Edit Window Views", item] });
            }
        });
    }
    
    // Helper: Spanner letter (fx a–j)
    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);
        }
        return null;
    }
    
    // Helper: Select Spanner lane
    function selectSpannerAutomationLane(laneName) {
        const insertLetter = getSpannerInsertLetter();
        if (!insertLetter) throw `Spanner plugin not found on selected track.`;
        sf.ui.proTools.selectedTrack.trackScrollToView();
        sf.ui.proTools.selectedTrack.trackDisplaySelect({
            displayPath: [` (fx ${insertLetter}) Spanner`, laneName]
        });
        return true;
    }
    
    // Helper: Select pan lane
    function selectPanAutomationLaneForPasting(spannerLaneName) {
        const laneMap = {
            "PanX_": ["front pos", "rear pos"],
            "PanY_": ["f/r pos"],
            "PanZ_": ["height"]
        };
        for (const [prefix, lanes] of Object.entries(laneMap)) {
            if (spannerLaneName.startsWith(prefix)) {
                sf.ui.proTools.selectedTrack.trackDisplaySelect({ displayPath: ["pan", lanes[0]] });
                return lanes;
            }
        }
        return [];
    }
    
    // Select relative track
    function selectTrackDelta(delta) {
        const selectedName = sf.ui.proTools.selectedTrackNames[0];
        const visibleNames = sf.ui.proTools.visibleTrackNames;
        const idx = visibleNames.findIndex(n => n === selectedName) + delta;
        if (idx >= 0 && idx < visibleNames.length) {
            sf.ui.proTools.trackSelectByName({ names: [visibleNames[idx]] });
        }
    }
    
    // Timeline selection check
    function checkSelectionLength() {
        const selection = sf.ui.proTools.selectionGet();
        const length = selection.selectionLength.split(":").slice(2).join(".");
        return parseFloat(length) !== 0;
    }
    
    function panPaste() {
        sf.keyboard.press({ keys: "ctrl+cmd+v" });
    }
    
    // ─── MAIN ───
    sf.ui.proTools.appActivateMainWindow();
    ensureAllInsertsAreDisplayed();
    
    if (!checkSelectionLength()) {
        alert("No selection in timeline.");
        throw 0;
    }
    
    selectTrackDelta(0);
    let currentDelta = 1;
    let pasteCounter = 0;
    
    sf.interaction.notify({
        uid: notificationID,
        title: `Processing Automation Data`,
        message: `Track format: ${trWidth} | Speakers: ${panCount}`,
        keepAliveMs: 60000
    });
    
    for (let i = 0; i < filteredSpannerLaneNames.length; i++) {
        const lane = filteredSpannerLaneNames[i];
        try {
            if (selectSpannerAutomationLane(lane)) {
                const channelID = lane.split('_')[1];
    
                sf.interaction.notify({
                    uid: notificationID,
                    title: "Processing Automation Data",
                    progress: i / filteredSpannerLaneNames.length,
                    message: `Pasting ${channelID} (${Math.round((pasteCounter / (panCount * 4)) * 100)}% done)`
                });
    
                sf.keyboard.press({ keys: "cmd+c" });
                selectTrackDelta(currentDelta);
    
                const panLanes = selectPanAutomationLaneForPasting(lane);
                panLanes.forEach(pl => {
                    sf.ui.proTools.selectedTrack.trackDisplaySelect({ displayPath: ["pan", pl] });
                    sf.wait({ intervalMs: 100 })
                    panPaste();
                    pasteCounter++;
                });
    
                selectTrackDelta(-currentDelta);
                if ((i + 1) % 3 === 0) currentDelta++;
            }
        } catch (err) {
            continue;
        }
    }
    
    sf.interaction.notify({
        uid: notificationID,
        title: "Spanner Conversion Complete",
        message: `Automation transferred for ${trWidth} format.`,
        keepAliveMs: 5000
    });
    
    • 0 replies