No internet connection
  1. Home
  2. Script Sharing

Detect onset of audio within multiple audio tracks

By danielkassulke @danielkassulke
    2025-04-20 01:41:29.613Z

    Hilariously I was trying to work out how to tackle this script for so long that I forgot what my need was for, but I can imagine many scenarios that it will be helpful. This script uses FFmpeg to analyse the audio, then drops a marker with very high accuracy at the onset of actual audio across each clip in your session. From my initial testing, this is more accurate than tabbing to transient in terms of finding the precise onset of audio. On line 49 you can adjust the threshold for detecting audio - your milage may vary. -80dB works for me. Note that I also offset the detection of audio by -1 sample on line 99, which may not be needed if you can find a low enough detection threshold that works for you.

    const pT = sf.ui.proTools;
    pT.appActivateMainWindow();
    
    function getSessionSampleRate() {
        const sr = sf.app.proTools.getSessionSampleRate().sampleRate;
        const numericRate = Number(String(sr).match(/\d+/)[0]);
        if (isNaN(numericRate)) throw `Invalid sample rate: ${sr}`;
        return numericRate;
    }
    
    function isAudioTrack(track) {
        return track.title.value.indexOf('Audio Track') >= 0;
    }
    
    function selectAllAudioTracks() {
        sf.ui.proTools.mainWindow.invalidate();
        const tracksToSelect = sf.ui.proTools.visibleTrackHeaders.filter(isAudioTrack);
        sf.ui.proTools.trackSelectByName({
            names: tracksToSelect.map(t => t.normalizedTrackName),
            deselectOthers: true,
        });
    }
    
    function getFirstClipInfo(trackName) {
        sf.ui.proTools.mainWindow.invalidate();
        sf.app.proTools.selectTracksByName({
            trackNames: [trackName],
            selectionMode: 'Replace',
        });
    
        sf.ui.proTools.menuClick({
            menuPath: ["Edit", "Select All"],
        });
    
        const selectedClips = sf.app.proTools.getSelectedClipInfo().Clips;
        if (!selectedClips.length) throw `No clips found on track "${trackName}"`;
    
        const firstClip = selectedClips[0];
        const clipStartTime = firstClip.startTime;
    
        const audioFilePath = sf.app.proTools.getFileLocation({
            fileFilters: ['SelectedClipsTimeline'],
        }).fileLocations[0].path;
    
        return { clipStartTime, audioFilePath };
    }
    
    function getFirstAudioOnsetSeconds(audioFilePath) {
        const commandLine = `/opt/homebrew/bin/ffmpeg -i "${audioFilePath}" -af silencedetect=noise=-80dB:d=0.02 -f null - 2>&1`;
        const ffmpegResult = sf.system.exec({ commandLine }).result;
        const silenceEndMatch = ffmpegResult.match(/silence_end:\s*([\d.]+)/);
        return silenceEndMatch && silenceEndMatch[1] ? parseFloat(silenceEndMatch[1]) : null;
    }
    
    function getNextAvailableMarkerNumber() {
        sf.app.proTools.memoryLocations.invalidate();
        const existingNumbers = sf.app.proTools.memoryLocations.allItems.map(loc => loc.number);
        let next = 1;
        while (existingNumbers.includes(next)) next++;
        return next;
    }
    
    function createMarkerAtSample(sample, name) {
        const number = getNextAvailableMarkerNumber();
    
        sf.app.proTools.createMemoryLocation({
            startTime: sample.toString(),
            timeProperties: "Marker",
            name: name,
            number: number
        });
    
        sf.waitFor({
            callback: () => {
                sf.app.proTools.memoryLocations.invalidate();
                return sf.app.proTools.memoryLocations.allItems.some(loc => loc.number === number);
            },
            timeout: 1000
        });
    }
    
    function createOnsetMarkers() {
        const sampleRate = getSessionSampleRate();
    
        pT.tracks.invalidate();
        sf.ui.proTools.mainWindow.invalidate();
        selectAllAudioTracks();
    
        sf.ui.proTools.mainWindow.invalidate();
        const audioTracks = sf.ui.proTools.visibleTrackHeaders.filter(isAudioTrack);
        const trackNames = audioTracks.map(t => t.normalizedTrackName);
    
        for (const trackName of trackNames) {
            try {
                const { clipStartTime, audioFilePath } = getFirstClipInfo(trackName);
                const onsetSecondsRelative = getFirstAudioOnsetSeconds(audioFilePath);
                if (onsetSecondsRelative !== null) {
                    const onsetSamplesRelative = Math.round(onsetSecondsRelative * sampleRate);
                    const onsetSamplesAbsolute = Math.max(0, Math.round(clipStartTime + onsetSamplesRelative) - 1);
                    createMarkerAtSample(onsetSamplesAbsolute, `${trackName} audio start`);
                }
            } catch (err) {}
        }
    }
    
    createOnsetMarkers();
    
    • 0 replies