No internet connection
  1. Home
  2. Script Sharing

Delete Muted Clips from Selected Tracks

By Sreejesh Nair @Sreejesh_Nair
    2026-08-26 10:14:09.187Z2026-08-27 11:10:32.158Z

    This is a method I use in the Atmos Mixing Package. I parse the session info and the selected tracks' EDLs in a single getSessionInfoAsText call, then read the STATE column to find every muted clip along with any fade that touches one. Those get merged into the fewest possible timeline ranges. It is clips that touch collapse into one range, so do the stretches of empty timeline between them, and any range that repeats across several tracks is batched so a single clear covers all of them. Each range is then deleted with setTimelineSelection and clear(). Main counter, edit mode, Link Timeline and Edit Selection, track selection and timeline selection are all captured up front and restored so the session ends up exactly as it was found.

    It intelligently finds all the bunchable clips it can delete and proceeds that way to minimise the deletion time. If there is a timeline selection, it does it only on those tracks and in that timeline selection. If there are only track selections, then all the clips on those selected tracks are analysed. If there is no selection, it does it across the whole session.

    // Delete Muted Clips
    
    const PT = sf.app.proTools;
    
    const CONFIG = {
        deleteAdjacentFades: true,
        mergeAcrossGaps: true,
        confirm: true,
    };
    
    function parseTrackEdls(sessionInfo) {
        const tracks = [];
        let track = null;
        let columns = null;
    
        sessionInfo.split('\n').forEach(rawLine => {
            const line = rawLine.replace(/\r$/, '');
    
            if (line.indexOf('TRACK NAME:') === 0) {
                track = { edlName: (line.split('\t')[1] || '').trim(), clips: [] };
                columns = null;
                tracks.push(track);
                return;
            }
    
            if (!track) return;
    
            if (line.indexOf('CHANNEL') === 0) {
                columns = {};
                line.split('\t').forEach((heading, index) => {
                    columns[heading.trim().toUpperCase()] = index;
                });
                return;
            }
    
            if (!columns) return;
    
            const cells = line.split('\t');
            if (cells.length < 2) return; // blank line: end of this track's EDL
    
            const cell = name =>
                columns[name] === undefined ? '' : (cells[columns[name]] || '').trim();
    
            const start = Number(cell('START TIME').replace(/\s/g, ''));
            const end = Number(cell('END TIME').replace(/\s/g, ''));
            if (!isFinite(start) || !isFinite(end)) return;
    
            const name = cell('CLIP NAME');
    
            track.clips.push({
                name,
                start,
                end,
                // "Unmuted" contains "Muted", so compare the whole field, never a substring.
                isMuted: cell('STATE').toLowerCase() === 'muted',
                isFade: isFadeClipName(name),
            });
        });
    
        return tracks;
    }
    
    function isFadeClipName(name) {
        return /\([^()]*fade[^()]*\)/i.test(name);
    }
    
    function planTrack(clips) {
        // Multichannel tracks list the same clip once per channel.
        const seen = {};
        const sorted = clips
            .filter(clip => {
                if (clip.end <= clip.start) return false;
                const key = clip.start + '|' + clip.end;
                if (seen[key]) return false;
                seen[key] = true;
                return true;
            })
            .sort((a, b) => a.start - b.start);
    
        const toDelete = sorted.map(clip => clip.isMuted);
    
        if (CONFIG.deleteAdjacentFades) {
            // Deletion spreads from muted clips through touching fades, one sweep each way.
            for (let i = 1; i < sorted.length; i++) {
                if (!toDelete[i] && sorted[i].isFade &&
                    toDelete[i - 1] && sorted[i - 1].end >= sorted[i].start) toDelete[i] = true;
            }
            for (let i = sorted.length - 2; i >= 0; i--) {
                if (!toDelete[i] && sorted[i].isFade &&
                    toDelete[i + 1] && sorted[i + 1].start <= sorted[i].end) toDelete[i] = true;
            }
        }
    
        return {
            toDelete: sorted.filter((clip, i) => toDelete[i]),
            toKeep: sorted.filter((clip, i) => !toDelete[i]),
        };
    }
    
    function mergeRanges(toDelete, toKeep) {
        const ranges = [];
        let next = 0; // first kept clip that might still block a merge
    
        toDelete.forEach(clip => {
            const last = ranges[ranges.length - 1];
    
            if (!last) {
                ranges.push([clip.start, clip.end]);
                return;
            }
    
            if (clip.start <= last[1]) {
                last[1] = Math.max(last[1], clip.end);
                return;
            }
    
            if (!CONFIG.mergeAcrossGaps) {
                ranges.push([clip.start, clip.end]);
                return;
            }
    
            while (next < toKeep.length && toKeep[next].end <= last[1]) next++;
            const blocked = next < toKeep.length && toKeep[next].start < clip.start;
    
            if (blocked) ranges.push([clip.start, clip.end]);
            else last[1] = clip.end;
        });
    
        return ranges;
    }
    
    function buildCutPlan(jobs) {
        const byRange = {};
    
        jobs.forEach(job => {
            job.ranges.forEach(range => {
                const key = range[0] + '|' + range[1];
                if (!byRange[key]) byRange[key] = { start: range[0], end: range[1], tracks: [] };
                byRange[key].tracks.push(job.trackName);
            });
        });
    
        return Object.keys(byRange)
            .map(key => {
                const entry = byRange[key];
                entry.tracks.sort();
                entry.trackKey = entry.tracks.join(' ');
                return entry;
            })
            .sort((a, b) => {
                if (a.trackKey !== b.trackKey) return a.trackKey < b.trackKey ? -1 : 1;
                return b.start - a.start;
            });
    }
    
    function makeTrackNameResolver(realNames) {
        return edlName => {
            if (realNames.indexOf(edlName) !== -1) return edlName;
            const stripped = edlName.replace(/\s*\([^()]*\)\s*$/, '');
            return realNames.indexOf(stripped) !== -1 ? stripped : edlName;
        };
    }
    
    function setLinkTimelineAndEditSelection(enabled) {
        try {
            return sf.ui.proTools.toolsSetMode({
                mode: 'LinkTimelineAndEditSelection',
                targetValue: enabled ? 'Enable' : 'Disable',
            }).oldValue;
        } catch (err) {
            PT.setEditModeOptions({ linkTimelineAndEditSelection: enabled });
            return null;
        }
    }
    
    function withSessionState(action) {
        // Capture and restore the selection while the counter is in Samples.
        const originalCounter = PT.getMainCounterFormat().currentSetting;
        PT.setMainCounterFormat({ value: 'Samples' });
    
        const originalLink = setLinkTimelineAndEditSelection(true);
    
        const originalSelection = PT.getTimelineSelection();
        const allTracks = PT.tracks.invalidate().allItems;
        const realNames = allTracks.map(t => t.name).filter(Boolean);
        const originalTrackNames = allTracks
            .filter(t => t.isSelected)
            .map(t => t.name);
    
        // Selected tracks and a nonzero timeline selection narrow the scope.
        const scope = {
            trackList: originalTrackNames.length ? 'SelectedTracksOnly' : 'AllTracks',
            range: originalSelection && originalSelection.inTime !== originalSelection.outTime
                ? { start: Number(originalSelection.inTime), end: Number(originalSelection.outTime) }
                : null,
        };
    
        try {
            action(realNames, scope);
        } finally {
            if (originalTrackNames.length) {
                PT.selectTracksByName({
                    trackNames: originalTrackNames,
                    selectionMode: 'Replace',
                });
            } else if (realNames.length) {
                // Nothing was selected before, so leave nothing selected.
                PT.selectTracksByName({
                    trackNames: realNames,
                    selectionMode: 'Subtract',
                });
            }
            if (originalSelection && originalSelection.inTime) {
                PT.setTimelineSelection({
                    inTime: originalSelection.inTime,
                    outTime: originalSelection.outTime,
                });
            }
            if (originalLink === false) {
                setLinkTimelineAndEditSelection(false);
            }
            if (originalCounter && originalCounter !== 'Samples') {
                PT.setMainCounterFormat({ value: originalCounter });
            }
        }
    }
    
    function runInSlipMode(action) {
        let started = false;
        const guarded = () => { started = true; action(); };
    
        try {
            sf.ui.proTools.editModeDoWith({ targetValue: 'Slip', action: guarded });
        } catch (err) {
            if (started) throw err; // the failure came from the action, don't rerun it
            PT.setEditMode({ editMode: 'Slip' });
            guarded();
        }
    }
    
    function collectJobs(realNames, scope) {
        const sessionInfo = PT.getSessionInfoAsText({
            includeClipList: false,
            includeFileList: false,
            includeMarkers: false,
            includePluginList: false,
            includeUserTimestamps: false,
            includeTrackEdls: true,
            trackList: scope.trackList,
            trackOffsetOptions: 'Samples',
            // Fades must be their own EDL events for planTrack to find them.
            crossFadeHandling: 'ShowCrossfades',
        }).sessionInfo;
    
        const resolveName = makeTrackNameResolver(realNames);
    
        return parseTrackEdls(sessionInfo)
            .map(track => {
                const clips = scope.range
                    ? track.clips.filter(c => c.end > scope.range.start && c.start < scope.range.end)
                    : track.clips;
                const plan = planTrack(clips);
                let ranges = mergeRanges(plan.toDelete, plan.toKeep);
                if (scope.range) {
                    // Never clear outside the timeline selection.
                    ranges = ranges.map(r => [
                        Math.max(r[0], scope.range.start),
                        Math.min(r[1], scope.range.end),
                    ]);
                }
                return {
                    trackName: resolveName(track.edlName),
                    clips: plan.toDelete,
                    ranges,
                };
            })
            .filter(job => job.clips.length > 0);
    }
    
    function executeCutPlan(cutPlan, notify) {
        let selectedTracks = null;
    
        runInSlipMode(() => {
            cutPlan.forEach((entry, index) => {
                if (entry.trackKey !== selectedTracks) {
                    PT.selectTracksByName({
                        trackNames: entry.tracks,
                        selectionMode: 'Replace',
                    });
                    selectedTracks = entry.trackKey;
                }
    
                PT.setTimelineSelection({
                    inTime: String(entry.start),
                    outTime: String(entry.end),
                });
                PT.clear();
    
                if (index % 10 === 0 || index === cutPlan.length - 1) {
                    notify(`${index + 1} of ${cutPlan.length} edits`, (index + 1) / cutPlan.length);
                }
            });
        });
    }
    
    function main() {
        sf.ui.proTools.appActivateMainWindow();
        PT.requireOpenSession();
    
        const notificationId = sf.system.newGuid().guid;
        const notify = (message, progress) => sf.interaction.notify({
            uid: notificationId,
            title: 'Delete Muted Clips',
            message,
            progress,
        });
        const count = (n, noun) => `${n} ${noun}${n === 1 ? '' : 's'}`;
    
        withSessionState((realNames, scope) => {
            const jobs = collectJobs(realNames, scope);
            const clipCount = jobs.reduce((sum, job) => sum + job.clips.length, 0);
            const tally = `${count(clipCount, 'clip')} on ${count(jobs.length, 'track')}`;
    
            if (clipCount === 0) {
                notify(scope.trackList === 'SelectedTracksOnly'
                    ? 'No muted clips on the selected tracks.'
                    : 'No muted clips in the session.');
                return;
            }
    
            if (CONFIG.confirm) {
                const answer = sf.interaction.displayDialog({
                    title: 'Delete Muted Clips',
                    prompt: `Delete ${tally}?`,
                    buttons: ['Cancel', 'Delete'],
                    defaultButton: 'Delete',
                    cancelButton: 'Cancel',
                    onCancel: 'Continue',
                });
                if (!answer || answer.button !== 'Delete') return;
            }
    
            executeCutPlan(buildCutPlan(jobs), notify);
            notify(`Deleted ${tally}.`, 1);
        });
    }
    
    main();
    
    • 0 replies