📄 audio.js
/home/palash/git/site/src/components/tab-viewer/audio.js
Language: js • Lines: 873
// --- Static Configuration Matrix ---
export const TUNING = {
    1: { baseMidi: 69 }, // A4
    2: { baseMidi: 64 }, // E4
    3: { baseMidi: 60 }, // C4 (Middle C)
    4: { baseMidi: 55 }, // G3
    5: { baseMidi: 50 }, // D3
    6: { baseMidi: 45 }  // A2
};

export const NOTE_NAMES = [
    "C",
    "C#",
    "D",
    "D#",
    "E",
    "F",
    "F#",
    "G",
    "G#",
    "A",
    "A#",
    "B"
];

export const RHYTHM_BEAT_VALUES = {
    o: 4.0,
    "o.": 6.0,
    ".": 2.0,
    "..": 3.0,
    ":": 1.0,
    ":.": 1.5,
    "+": 0.5,
    "+.": 0.75,
    "=": 0.25,
    r: 1.0,
    "r.": 1.5,
    "r+": 0.5,
    "r=": 0.25
};

// Voice pool: pre-allocated nodes reused across notes — zero GC during playback
const VOICE_COUNT = 12;
let voicePool = null;

// Shared noise buffer for finger-attack transient
let noiseBuffer = null;

function getNoiseBuffer(ctx) {
    if (!noiseBuffer) {
        const sr = ctx.sampleRate;
        const length = Math.ceil(sr * 0.06);
        noiseBuffer = ctx.createBuffer(1, length, sr);
        const data = noiseBuffer.getChannelData(0);
        for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
    }
    return noiseBuffer;
}

function acquireVoice(ctx, voiceStartTime) {
    const now = ctx.currentTime;
    const scheduleBase = Math.max(voiceStartTime, now);

    if (!voicePool) {
        voicePool = [];
        const masterTarget = getMasterGain(ctx);
        for (let i = 0; i < VOICE_COUNT; i++) {
            const osc = ctx.createOscillator();
            osc.type = 'triangle';

            const gain = ctx.createGain();
            gain.gain.value = 0;

            const filter = ctx.createBiquadFilter();
            filter.type = 'lowpass';
            filter.frequency.value = 1000;

            const bodyOsc = ctx.createOscillator();
            bodyOsc.type = 'triangle';

            const bodyGain = ctx.createGain();
            bodyGain.gain.value = 0;

            osc.connect(gain);
            bodyOsc.connect(bodyGain);
            bodyGain.connect(gain);
            gain.connect(filter);
            filter.connect(masterTarget);

            osc.start();
            bodyOsc.start();

            voicePool.push({ osc, bodyOsc, gain, bodyGain, filter, releaseTime: 0 });
        }
    }

    // Find available voice (releaseTime <= scheduleBase) or steal the soonest-free
    let best = null;
    for (const v of voicePool) {
        if (v.releaseTime <= scheduleBase) { best = v; break; }
    }
    if (!best) {
        best = voicePool.reduce((a, b) => a.releaseTime < b.releaseTime ? a : b);
    }

    const resetTime = scheduleBase;

    best.gain.gain.cancelScheduledValues(resetTime);
    best.bodyGain.gain.cancelScheduledValues(resetTime);
    best.filter.frequency.cancelScheduledValues(resetTime);
    best.osc.frequency.cancelScheduledValues(resetTime);
    best.bodyOsc.frequency.cancelScheduledValues(resetTime);

    best.gain.gain.setValueAtTime(0, resetTime);
    best.bodyGain.gain.setValueAtTime(0, resetTime);

    return best;
}

// Minimal cleanup queue — only used by the rare playMutedPercussion path
const pendingNodeCleanups = [];
const drainNodeCleanups = (currentTime) => {
    while (pendingNodeCleanups.length > 0 && pendingNodeCleanups[0].time <= currentTime) {
        pendingNodeCleanups.shift().cleanup();
    }
};

// Global Node Manager: Clean audio summation channel with a safety compressor
const getMasterGain = (ctx) => {
    if (!ctx.masterGain) {
        // Global mix gain with proper headroom for multi-note polyphony
        const masterGain = ctx.createGain();
        masterGain.gain.value = 0.50;

        const bodyWarmth = ctx.createBiquadFilter();
        bodyWarmth.type = 'lowshelf';
        bodyWarmth.frequency.value = 350;
        bodyWarmth.gain.value = 3.5;

        const woodyResonance = ctx.createBiquadFilter();
        woodyResonance.type = 'peaking';
        woodyResonance.frequency.value = 500;
        woodyResonance.Q.value = 0.7;
        woodyResonance.gain.value = 0.8;

        const limiter = ctx.createDynamicsCompressor();
        limiter.threshold.setValueAtTime(-12.0, ctx.currentTime);
        limiter.knee.setValueAtTime(12.0, ctx.currentTime);
        limiter.ratio.setValueAtTime(6.0, ctx.currentTime);
        limiter.attack.setValueAtTime(0.002, ctx.currentTime);
        limiter.release.setValueAtTime(0.10, ctx.currentTime);

        masterGain.connect(bodyWarmth);
        bodyWarmth.connect(woodyResonance);
        woodyResonance.connect(limiter);
        limiter.connect(ctx.destination);
        ctx.masterGain = masterGain;
    }
    return ctx.masterGain;
};

/**
 * Plays a single note or a chain of continuous articulations with realistic nylon string modeling.
 * Re-engineered for crisp acoustic clarity, zero-pop note terminations, and smooth 6-string polyphony.
 */
export const playHumanizedGuitaleleNote = (ctx, midiOrChain, startTime, duration, velocity = 1.0, noteVoice = 1) => {
    let segments = [];
    if (Array.isArray(midiOrChain)) {
        segments = midiOrChain;
    } else {
        segments = [{ type: 'pluck', midi: midiOrChain, duration: duration }];
    }

    const polyphonyScale = Array.isArray(midiOrChain) && midiOrChain.length > 1 ? 0.65 : 1.0;
    const effectiveVelocity = velocity * polyphonyScale * (noteVoice === 2 ? 0.65 : 1.0);
    const totalDuration = segments.reduce((sum, seg) => sum + (seg.duration || 0), 0);
    const firstPlayable = segments.find(s => typeof s.midi === 'number');

    const playMutedPercussion = (time, dur = 0.05, vel = 0.5) => {
        const lowThump = ctx.createOscillator();
        const highScrape = ctx.createOscillator();
        const filter = ctx.createBiquadFilter();
        const g = ctx.createGain();

        lowThump.type = 'triangle';
        lowThump.frequency.setValueAtTime(95, time);

        highScrape.type = 'sawtooth';
        highScrape.frequency.setValueAtTime(1400, time);
        highScrape.detune.setValueAtTime((Math.random() * 40) - 20, time);

        filter.type = 'bandpass';
        filter.frequency.setValueAtTime(800, time);
        filter.Q.value = 2.0;

        g.gain.setValueAtTime(vel * 0.18, time);
        g.gain.exponentialRampToValueAtTime(0.001, time + dur);

        lowThump.connect(g);
        highScrape.connect(filter);
        filter.connect(g);

        g.connect(getMasterGain(ctx));

        lowThump.start(time);
        highScrape.start(time);
        lowThump.stop(time + dur);
        highScrape.stop(time + dur);
        pendingNodeCleanups.push({
            time: time + dur + 0.02,
            cleanup: () => {
                try {
                    lowThump.disconnect();
                    highScrape.disconnect();
                    filter.disconnect();
                    g.disconnect();
                } catch (e) {}
            }
        });
    };

    if (!firstPlayable) {
        let cursor = startTime;
        for (let i = 0; i < segments.length; i++) {
            const seg = segments[i];
            const segDur = seg.duration || 0.25;
            if (seg.type === 'mute') playMutedPercussion(cursor, Math.min(0.06, segDur));
            cursor += segDur;
        }
        return;
    }

    if (segments.length > 0 && segments.every(s => s.type === 'mute')) return;

    const initialMidi = firstPlayable.midi;
    const initialFundamental = 440 * Math.pow(2, (initialMidi - 69) / 12);

    // Acquire a pre-allocated voice from the pool — zero node creation
    const voice = acquireVoice(ctx, startTime);

    voice.filter.frequency.setValueAtTime(Math.min(900, initialFundamental * 1.5), startTime);
    voice.filter.frequency.exponentialRampToValueAtTime(Math.min(220, initialFundamental * 0.6), startTime + Math.min(totalDuration, 0.5));

    // Finger-on-string attack noise (the critical "stab" transient)
    const noiseSrc = ctx.createBufferSource();
    noiseSrc.buffer = getNoiseBuffer(ctx);
    const noiseFilter = ctx.createBiquadFilter();
    noiseFilter.type = 'bandpass';
    noiseFilter.frequency.value = Math.min(1200, initialFundamental * 2.5);
    noiseFilter.Q.value = 1.2;
    const noiseGain = ctx.createGain();
    noiseGain.gain.setValueAtTime(0, startTime);
    noiseGain.gain.linearRampToValueAtTime(effectiveVelocity * 0.08, startTime + 0.002);
    noiseGain.gain.exponentialRampToValueAtTime(0.001, startTime + 0.025);
    noiseSrc.connect(noiseFilter);
    noiseFilter.connect(noiseGain);
    noiseGain.connect(voice.filter);
    noiseSrc.start(startTime);
    noiseSrc.stop(startTime + 0.06);
    pendingNodeCleanups.push({
        time: startTime + 0.08,
        cleanup: () => {
            try { noiseSrc.disconnect(); noiseFilter.disconnect(); noiseGain.disconnect(); } catch (e) {}
        }
    });

    // String stretch: pitch bends up ~12 cents at attack then settles (simulates string being displaced by pick)
    voice.osc.frequency.setValueAtTime(initialFundamental * 1.012, startTime);
    voice.osc.frequency.exponentialRampToValueAtTime(initialFundamental, startTime + 0.025);

    const attackTime = 0.010;
    voice.gain.gain.setValueAtTime(0, startTime);
    voice.gain.gain.linearRampToValueAtTime(effectiveVelocity * 0.60, startTime + attackTime);

    if (noteVoice === 2) {
        // V2: slow gradual decay (drone) — rings through the measure then fades
        voice.gain.gain.exponentialRampToValueAtTime(effectiveVelocity * 0.15, startTime + 0.08);
        const decayEnd = startTime + Math.max(totalDuration, 0.5);
        voice.gain.gain.exponentialRampToValueAtTime(0.001, decayEnd);
        voice.gain.gain.setValueAtTime(0.001, decayEnd);
        voice.gain.gain.linearRampToValueAtTime(0, decayEnd + 0.015);
        voice.releaseTime = decayEnd + 0.02;
    } else {
        // V1: natural decay
        const totalDecayTime = Math.max(totalDuration * 0.95, 1.2);
        voice.gain.gain.exponentialRampToValueAtTime(effectiveVelocity * 0.25, startTime + 0.06);
        voice.gain.gain.exponentialRampToValueAtTime(0.01, startTime + totalDecayTime - 0.03);
        const fadeOutTime = 0.015;
        const stopTime = startTime + totalDecayTime;
        voice.gain.gain.setValueAtTime(0.01, stopTime - fadeOutTime);
        voice.gain.gain.linearRampToValueAtTime(0, stopTime);
        voice.releaseTime = stopTime;
    }

    // Main oscillator
    const detuneA = (Math.random() * 6) - 3;
    voice.osc.detune.setValueAtTime(detuneA, startTime);

    // Body resonance: percussive thump at attack, then sustained drone
    const droneEnd = startTime + Math.max(totalDuration * 0.9, 0.3);
    voice.bodyGain.gain.setValueAtTime(effectiveVelocity * 0.50, startTime);
    voice.bodyGain.gain.exponentialRampToValueAtTime(effectiveVelocity * 0.18, startTime + 0.04);
    voice.bodyGain.gain.exponentialRampToValueAtTime(0.001, droneEnd);
    voice.bodyOsc.frequency.setValueAtTime(initialFundamental, startTime);
    voice.bodyOsc.detune.setValueAtTime((Math.random() * 8) - 4, startTime);

    // Continuous Pitch Timeline Automation
    let timeCursor = startTime;
    let currentMidi = initialMidi;

    for (let i = 0; i < segments.length; i++) {
        const seg = segments[i];
        const segmentStartTime = timeCursor;
        const segmentEndTime = timeCursor + seg.duration;
        const hasMidi = typeof seg.midi === 'number';

        if (seg.type === 'mute') {
            timeCursor = segmentEndTime;
            continue;
        }

        if (seg.type === 'slide') {
            const startFund = 440 * Math.pow(2, (currentMidi - 69) / 12);
            const targetFund = hasMidi ? 440 * Math.pow(2, (seg.midi - 69) / 12) : startFund;

            voice.osc.frequency.setValueAtTime(startFund, segmentStartTime);
            voice.bodyOsc.frequency.setValueAtTime(startFund, segmentStartTime);

            voice.osc.frequency.linearRampToValueAtTime(targetFund, segmentEndTime);
            voice.bodyOsc.frequency.linearRampToValueAtTime(targetFund, segmentEndTime);
            currentMidi = seg.midi;
        } else if (seg.type === 'hammer' || seg.type === 'pull') {
            if (hasMidi) {
                const targetFund = 440 * Math.pow(2, (seg.midi - 69) / 12);
                voice.osc.frequency.setValueAtTime(targetFund, segmentStartTime);
                voice.bodyOsc.frequency.setValueAtTime(targetFund, segmentStartTime);
                currentMidi = seg.midi;
                voice.osc.frequency.setValueAtTime(targetFund, segmentEndTime);
                voice.bodyOsc.frequency.setValueAtTime(targetFund, segmentEndTime);
            }
        } else {
            const currentFund = 440 * Math.pow(2, (currentMidi - 69) / 12);
            voice.osc.frequency.setValueAtTime(currentFund, segmentEndTime);
            voice.bodyOsc.frequency.setValueAtTime(currentFund, segmentEndTime);
        }
        timeCursor = segmentEndTime;
    }
};

const clearPlaybackCallbacks = playbackTimeoutsRef => {
    playbackTimeoutsRef.current.forEach(cancelScheduledWork => {
        cancelScheduledWork();
    });
    playbackTimeoutsRef.current = [];
};

const registerManagedTimeout = (playbackTimeoutsRef, callback, delayMs) => {
    let timeoutId = null;
    const cancelScheduledWork = () => {
        if (timeoutId !== null) {
            clearTimeout(timeoutId);
            timeoutId = null;
        }
    };

    timeoutId = setTimeout(() => {
        cancelScheduledWork();
        playbackTimeoutsRef.current = playbackTimeoutsRef.current.filter(
            cancel => cancel !== cancelScheduledWork
        );
        callback();
    }, delayMs);

    playbackTimeoutsRef.current.push(cancelScheduledWork);
    return cancelScheduledWork;
};

export function stopPlaying(lookaheadTimerRef, playbackTimeoutsRef, setIsPlaying, setIsPaused, setPlaybackIndex, pausedTimeRef, audioCtxRef) {
    return () => {
        if (lookaheadTimerRef.current) {
            clearTimeout(lookaheadTimerRef.current);
            lookaheadTimerRef.current = null;
        }
        clearPlaybackCallbacks(playbackTimeoutsRef);

        setIsPlaying(false);
        setIsPaused(false);
        setPlaybackIndex(null);
        pausedTimeRef.current = 0;

        // Drain all remaining node cleanups to reset the audio graph
        drainNodeCleanups(Infinity);

        // Close the AudioContext for a clean slate on next playback
        if (audioCtxRef.current && audioCtxRef.current.state !== "closed") {
            audioCtxRef.current.close();
        }
    };
}

export function pausePlaying(isPlaying, isPaused, lookaheadTimerRef, playbackTimeoutsRef, audioCtxRef, playbackStartTimeRef, pausedTimeRef, setIsPaused) {
    return () => {
        if (!isPlaying || isPaused) return;

        if (lookaheadTimerRef.current) {
            clearTimeout(lookaheadTimerRef.current);
            lookaheadTimerRef.current = null;
        }
        clearPlaybackCallbacks(playbackTimeoutsRef);

        const elapsedSec = audioCtxRef.current.currentTime - playbackStartTimeRef.current;
        pausedTimeRef.current += elapsedSec;

        setIsPaused(true);
        if (audioCtxRef.current) {
            audioCtxRef.current.suspend();
        }
    };
}

export function startPlaying(isPlaying, scoreLayout, isAudioCompiled, audioCtxRef, setIsPlaying, setIsPaused, pausedTimeRef, playbackStartTimeRef, currentPlaybackEventsRef, playbackStartBeatRef, preCompiledTimelineRef, currentTimelineBeatsRef, nextBeatIndexRef, runSchedulerLoop, voice1Enabled, voice2Enabled, metronomeEnabled) {
    return (fromMeasure = 1) => {
        if (isPlaying || !scoreLayout || !isAudioCompiled) return;

        // Drain any leftover node cleanups from previous playback
        drainNodeCleanups(Infinity);

        // Close old context and create a fresh one every time for a clean slate
        const oldCtx = audioCtxRef.current;
        if (oldCtx && oldCtx.state !== "closed") {
            oldCtx.close();
        }
        voicePool = null;
        const AC = window.AudioContext || window.webkitAudioContext;
        audioCtxRef.current = new AC({ latencyHint: 'playback', sampleRate: 44100 });

        setIsPlaying(true);
        setIsPaused(false);
        pausedTimeRef.current = 0;
        const ctx = audioCtxRef.current;
        playbackStartTimeRef.current = ctx.currentTime;

        const allEvents = scoreLayout.computedRows.flatMap(r => r.rowEvents);

        const targetedEvents = allEvents.filter(
            ev => ev.measureNumber >= fromMeasure
        );


        currentPlaybackEventsRef.current = targetedEvents;

        const startOffsetBeat = targetedEvents.length > 0 ? targetedEvents[0].startBeat : 0;
        playbackStartBeatRef.current = startOffsetBeat;

        // Group our precompiled notes by their exact startBeat timestamp
        const instrumentNotes = preCompiledTimelineRef.current.filter(
            n => n.startBeat >= startOffsetBeat
        ).filter(n => {
            if (n.voice === 1 && !voice1Enabled) return false;
            if (n.voice === 2 && !voice2Enabled) return false;
            return true;
        });

        const metronomeTicks = [];
        targetedEvents.forEach(ev => {
            const isExplicitTick = ev.isMetronomeTick;
            const isIntegerBeat = ev.beatOffset !== undefined && (ev.beatOffset % 1.0 === 0);

            if (isExplicitTick || isIntegerBeat) {
                const isDuplicate = metronomeTicks.some(tick => tick.startBeat === ev.startBeat);
                if (!isDuplicate) {
                    metronomeTicks.push({
                        startBeat: ev.startBeat,
                        globalIndex: ev.globalIndex,
                        voice: 0,
                        isMetronomeTick: true,
                        isDownbeat: ev.isDownbeat || (ev.beatOffset === 0),
                        segments: [],
                        preCalculatedJitter: 0,
                        preCalculatedVelocity: 1.0
                    });
                }
            }
        });


        // Add a count-in measure of metronome ticks that always play before content starts
        const numerator = parseInt(scoreLayout.timeSigTop, 10);
        const denominator = parseInt(scoreLayout.timeSigBottom, 10);
        const clickBeatSpacing = 4 / denominator;
        const beatsPerMeasure = scoreLayout.beatsPerMeasure;
        for (let click = 0; click < numerator; click++) {
            const clickStartBeat = startOffsetBeat - beatsPerMeasure + (click * clickBeatSpacing);
            metronomeTicks.unshift({
                startBeat: clickStartBeat,
                globalIndex: -(click + 1),
                voice: 0,
                isMetronomeTick: true,
                isPreviewTick: true,
                isDownbeat: click === 0,
                segments: [],
                preCalculatedJitter: 0,
                preCalculatedVelocity: 1.0
            });
        }

        const combinedTimeline = [...instrumentNotes, ...metronomeTicks];

        // Shift the playback start beat to the first preview tick so all scheduling times are non-negative
        const firstPreviewTickStart = startOffsetBeat - beatsPerMeasure;
        playbackStartBeatRef.current = firstPreviewTickStart;

        const uniqueBeatsMap = {};
        combinedTimeline.forEach(note => {
            if (!uniqueBeatsMap[note.startBeat]) {
                uniqueBeatsMap[note.startBeat] = {
                    startBeat: note.startBeat,
                    globalIndex: note.globalIndex, // Map to layout position for UI highlights
                    notes: []
                };
            }
            uniqueBeatsMap[note.startBeat].notes.push(note);
        });

        // Sort chronologically
        currentTimelineBeatsRef.current = Object.values(uniqueBeatsMap).sort(
            (a, b) => a.startBeat - b.startBeat
        );
        nextBeatIndexRef.current = 0;

        runSchedulerLoop(firstPreviewTickStart);
    };
}

export function resumePlaying(isPlaying, isPaused, audioCtxRef, playbackStartTimeRef, setIsPaused, runSchedulerLoop) {
    return () => {
        if (!isPlaying || !isPaused) return;

        if (audioCtxRef.current) {
            audioCtxRef.current.resume();
            playbackStartTimeRef.current = audioCtxRef.current.currentTime;
            setIsPaused(false);

            runSchedulerLoop();
        }
    };
}

/**
 * Synthesizes a highly prominent, punchy metronome click that cuts through the guitalele layers.
 */
export const playMetronomeClick = (ctx, startTime, isDownbeat = false) => {
    // 1. The Core Woodblock/Clave Body (Thump)
    const bodyOsc = ctx.createOscillator();
    const bodyGain = ctx.createGain();

    // Use a triangle wave for crisp harmonic presence instead of a soft sine wave
    bodyOsc.type = "triangle";
    // Pitch it up significantly so it sits in a different frequency range than the guitalele
    bodyOsc.frequency.setValueAtTime(isDownbeat ? 1400 : 950, startTime);

    // Ultra-snappy volume decay envelope to create a percussive "pop"
    bodyGain.gain.setValueAtTime(0.0, startTime);
    bodyGain.gain.linearRampToValueAtTime(isDownbeat ? 0.55 : 0.38, startTime + 0.002);
    bodyGain.gain.exponentialRampToValueAtTime(0.001, startTime + 0.035);

    // 2. The Transient Crack Layer (The high-frequency snap of a wood stick)
    const snapOsc = ctx.createOscillator();
    const snapGain = ctx.createGain();

    snapOsc.type = "sine";
    // Extreme high pitch creates an immediate piercing audio spike to catch the ear
    snapOsc.frequency.setValueAtTime(isDownbeat ? 3200 : 2600, startTime);

    snapGain.gain.setValueAtTime(0.0, startTime);
    snapGain.gain.linearRampToValueAtTime(isDownbeat ? 0.30 : 0.18, startTime + 0.001);
    snapGain.gain.exponentialRampToValueAtTime(0.001, startTime + 0.012); // Instant decay

    // Routing all elements to the output matrix
    bodyOsc.connect(bodyGain);
    snapOsc.connect(snapGain);

    if (ctx.masterGain) {
        bodyGain.connect(ctx.masterGain);
        snapGain.connect(ctx.masterGain);
    } else {
        bodyGain.connect(ctx.destination);
        snapGain.connect(ctx.destination);
    }

    // Fire nodes simultaneously
    bodyOsc.start(startTime);
    snapOsc.start(startTime);

    const bodyStop = startTime + 0.05;
    const snapStop = startTime + 0.02;
    bodyOsc.stop(bodyStop);
    snapOsc.stop(snapStop);

    pendingNodeCleanups.push({
        time: Math.max(bodyStop, snapStop) + 0.02,
        cleanup: () => {
            try {
                bodyOsc.disconnect();
                bodyGain.disconnect();
                snapOsc.disconnect();
                snapGain.disconnect();
            } catch (e) {}
        }
    });
};

/**
 * Synthesizes a short, crisp stick-click sound (drum sticks tapping together).
 * Uses a noise burst through a bandpass filter for a distinctly different timbre
 * from the regular metronome woodblock click.
 */
export const playStickClick = (ctx, startTime) => {
    const burstDuration = 0.025;
    const sampleRate = ctx.sampleRate;
    const bufferSize = Math.max(1, Math.ceil(sampleRate * burstDuration));
    const buffer = ctx.createBuffer(1, bufferSize, sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) {
        data[i] = (Math.random() * 2 - 1);
    }

    const noise = ctx.createBufferSource();
    noise.buffer = buffer;

    const bandpass = ctx.createBiquadFilter();
    bandpass.type = "bandpass";
    bandpass.frequency.setValueAtTime(3500, startTime);
    bandpass.Q.setValueAtTime(1.5, startTime);

    const gain = ctx.createGain();
    gain.gain.setValueAtTime(0.45, startTime);
    gain.gain.exponentialRampToValueAtTime(0.001, startTime + burstDuration);

    noise.connect(bandpass);
    bandpass.connect(gain);

    if (ctx.masterGain) {
        gain.connect(ctx.masterGain);
    } else {
        gain.connect(ctx.destination);
    }

    noise.start(startTime);
    const stopTime = startTime + burstDuration;
    noise.stop(stopTime);
    pendingNodeCleanups.push({
        time: stopTime + 0.02,
        cleanup: () => {
            try {
                noise.disconnect();
                bandpass.disconnect();
                gain.disconnect();
            } catch (e) {}
        }
    });
};
export function runScheduler(
    playbackStartBeatRef,
    audioCtxRef,
    bpm,
    playbackStartTimeRef,
    pausedTimeRef,
    nextBeatIndexRef,
    currentTimelineBeatsRef,
    scheduleAheadTime,
    setPlaybackIndex,
    playbackTimeoutsRef,
    stopPlayback,
    lookaheadTimerRef,
    lookaheadInterval,
    metronomeEnabled
) {
    return (startOffsetBeat = null) => {
        const offsetBeat = startOffsetBeat !== null
            ? startOffsetBeat
            : playbackStartBeatRef.current;
        const ctx = audioCtxRef.current;
        const beatDurationSeconds = 60 / bpm;
        const scheduleOffsetSec = 0.5; // 500ms padding for smooth scheduling on heavy layouts
        let nextTickMs = performance.now();
        const visualQueue = [];
        let visualFrameId = null;

        const cancelVisualQueue = () => {
            if (visualFrameId !== null) {
                cancelAnimationFrame(visualFrameId);
                visualFrameId = null;
            }
            visualQueue.length = 0;
        };

        playbackTimeoutsRef.current.push(cancelVisualQueue);

        const insertVisualUpdate = visualUpdate => {
            const insertIndex = visualQueue.findIndex(
                queuedUpdate => queuedUpdate.audioTime > visualUpdate.audioTime
            );

            if (insertIndex === -1) {
                visualQueue.push(visualUpdate);
            } else {
                visualQueue.splice(insertIndex, 0, visualUpdate);
            }
        };

        const startVisualQueue = () => {
            if (visualFrameId !== null) return;

            const runVisualFrame = () => {
                if (!ctx || ctx.state === "closed") {
                    visualFrameId = null;
                    return;
                }

                let nextPlaybackIndex = null;
                const currentAudioTime = ctx.currentTime;

                while (
                    visualQueue.length > 0 &&
                    visualQueue[0].audioTime <= currentAudioTime
                ) {
                    nextPlaybackIndex = visualQueue.shift().globalIndex;
                }

                if (nextPlaybackIndex !== null) {
                    setPlaybackIndex(nextPlaybackIndex);
                }

                visualFrameId = visualQueue.length > 0
                    ? requestAnimationFrame(runVisualFrame)
                    : null;
            };

            visualFrameId = requestAnimationFrame(runVisualFrame);
        };

        const queueVisualUpdate = (audioTime, globalIndex) => {
            insertVisualUpdate({ audioTime, globalIndex });
            startVisualQueue();
        };

        const scheduleTimelineChunk = () => {
            if (!ctx || ctx.state === "closed") return;

            drainNodeCleanups(ctx.currentTime);

            const absoluteCurrentPlaybackTime = ctx.currentTime -
                playbackStartTimeRef.current +
                pausedTimeRef.current;

            let iterations = 0;
            const MAX_ITERATIONS_PER_TICK = 12;
            const timelineLength = currentTimelineBeatsRef.current.length;
            const targetHorizonTime = absoluteCurrentPlaybackTime + scheduleAheadTime;

            while (nextBeatIndexRef.current < timelineLength && iterations < MAX_ITERATIONS_PER_TICK) {
                const beatSlice = currentTimelineBeatsRef.current[nextBeatIndexRef.current];

                if (!beatSlice) {
                    nextBeatIndexRef.current++;
                    continue;
                }

                const eventAbsoluteSec = (beatSlice.startBeat - offsetBeat) * beatDurationSeconds;

                // If this note's execution window is in the future, stop checking completely.
                if (eventAbsoluteSec >= targetHorizonTime) {
                    break;
                }

                // Increment tracking immediately to prevent infinite loops
                nextBeatIndexRef.current++;
                iterations++;

                const fallbackNote = beatSlice.notes[0];
                const jitter = fallbackNote ? fallbackNote.preCalculatedJitter : 0;
                const finalPluckTime = playbackStartTimeRef.current -
                    pausedTimeRef.current +
                    scheduleOffsetSec +
                    eventAbsoluteSec +
                    jitter;

                // Skip events that can't be scheduled in the future — the scheduler fell behind
                if (finalPluckTime < ctx.currentTime) continue;

                // 1. Dispatch audio nodes instantly to the Web Audio timeline queue
                beatSlice.notes.forEach(note => {
                    // This is the correct, safely timed metronome handler:
                    if (note.isMetronomeTick && (metronomeEnabled || note.isPreviewTick)) {
                        if (note.isPreviewTick) {
                            playStickClick(ctx, finalPluckTime);
                        } else {
                            playMetronomeClick(ctx, finalPluckTime, note.isDownbeat);
                        }
                        return;
                    }

                    const runtimeSegments = note.segments.map(seg => ({
                        ...seg,
                        duration: seg.duration * beatDurationSeconds
                    }));

                    playHumanizedGuitaleleNote(
                        ctx,
                        runtimeSegments,
                        finalPluckTime,
                        null,
                        note.type === "mute" ? 0 : note.preCalculatedVelocity,
                        note.voice || 1
                    );
                });

                // 2. High-precision visual state synchronization tracking
                const visualAudioTime = playbackStartTimeRef.current -
                    pausedTimeRef.current +
                    scheduleOffsetSec +
                    eventAbsoluteSec;
                queueVisualUpdate(visualAudioTime, beatSlice.globalIndex);

                // Process tied notes visual tracking efficiently
                beatSlice.notes.forEach(note => {
                    if (note.isMetronomeTick) return;
                    note.segments.forEach(seg => {
                        if (seg.tiedEventIndices) {
                            seg.tiedEventIndices.forEach(tiedEvent => {
                                const tiedAbsoluteSec = eventAbsoluteSec + tiedEvent.beatOffset * beatDurationSeconds;
                                const tiedVisualAudioTime = playbackStartTimeRef.current -
                                    pausedTimeRef.current +
                                    scheduleOffsetSec +
                                    tiedAbsoluteSec;
                                queueVisualUpdate(tiedVisualAudioTime, tiedEvent.globalIndex);
                            });
                        }
                    });
                });
            }

            // End tracking termination
            if (nextBeatIndexRef.current >= timelineLength) {
                const lastSlice = currentTimelineBeatsRef.current[timelineLength - 1];
                if (lastSlice) {
                    const maxSustainBeats = Math.max(
                        ...lastSlice.notes.map(n => n.segments ? n.segments.reduce((acc, s) => acc + s.duration, 0) : 1.0),
                        1.0
                    );

                    const totalDurationSec = (lastSlice.startBeat - offsetBeat + maxSustainBeats) * beatDurationSeconds;
                    const timeUntilEndMs = (totalDurationSec - absoluteCurrentPlaybackTime + scheduleOffsetSec) * 1000;

                    registerManagedTimeout(playbackTimeoutsRef, () => {
                        stopPlayback();
                    }, Math.max(0, timeUntilEndMs));
                }
                return;
            }

            // Drift-compensated lookahead: keeps interval consistent regardless of how long this tick took
            nextTickMs += lookaheadInterval;
            lookaheadTimerRef.current = setTimeout(
                scheduleTimelineChunk,
                Math.max(0, nextTickMs - performance.now())
            );
        };

        scheduleTimelineChunk();
    };
}