(function () {
    const RENDER_BLOCK = 512;
    const QUEUE_TARGET = 16;
    let _audioCtx = null;
    let _fluidModule = null;
    let _synthPtr = null;
    let _settingsPtr = null;
    let _workletNode = null;
    let _initialized = false;
    let _initPromise = null;
    let _currentSfId = null;
    let _sfHandleMap = new Map();
    let _channels = Array.from({ length: 16 }, () => ({ bank: 0, program: 0, isPercussion: false }));
    let _nextMelodicChannel = 0;
    let _sustainStates = new Array(16).fill(false);
    let _engineChMap = {};
    let _activeNotes = {};
    let _leftBufPtr = null;
    let _rightBufPtr = null;
    let _renderTimer = null;
    let _pendingNoteTimers = [];
    let _loadedFonts = {};
    let _activeOscillators = {};
    // Fallback oscillator theo 'channel:pitch' — để stopNote/stopAll dừng được
    // note khi thả phím (trước đây stopNote không dừng oscillator → âm kêu
    // liên tục dù đã thả phím khi FluidSynth chưa sẵn sàng / font load fail).
    let _activeOscillatorsByKey = {};
    // Epoch chống cache channel STALE: tăng mỗi khi selectInstrument/load
    // soundfont — channel cache chỉ được tin khi khớp epoch hiện tại, nếu
    // không → note KẾ TIẾP luôn program_select lại (hết "chọn Synth String
    // nghe Piano" do cache cũ skip program_select).
    let _instrumentEpoch = 0;
    let _gainNode = null;
    let _pendingOutputDestination = null;
    let _outputDestination = null; // cache đích route — dedupe swap dư giữa stream
    let _validPercCache = {};      // { sfId: [bank, prog] | null } — preset percussion hợp lệ
    let _sfLoadFailAt = {};        // { sfId: timestamp } — cooldown 10s sau load fail
    let _scheduledNotes = [];
    let _loadPromises = {};
    let _sfloadSeq = 0;
    // Note đang CHỜ load soundfont (chưa noteon) — 'ch:pitch' → số lần chờ.
    // Đăng ký TRƯỚC khi load để stopNote/stopAll/panic hủy được note này;
    // trước đây note deferred bắn TRỄ sau khi thả phím / sau Stop → âm loop
    // không dừng (keybed preview âm soundfont).
    let _pendingNoteOns = {};
    // Tăng mỗi lần stopAll/panic — deferred doNote của generation cũ thành no-op.
    let _noteGeneration = 0;

    const getCtx = function () {
        if (_audioCtx) {
            if (!_gainNode) {
                _gainNode = _audioCtx.createGain();
                _gainNode.gain.value = 0.3;
                _outputDestination = _pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination);
                _gainNode.connect(_outputDestination);
            }
            return _audioCtx;
        }
        if (typeof getAudioContext === 'function') {
            var ctx = getAudioContext();
            if (!_gainNode) {
                _gainNode = ctx.createGain();
                _gainNode.gain.value = 0.3;
                _outputDestination = _pendingOutputDestination || (window.masterBus ? window.masterBus.input : ctx.destination);
                _gainNode.connect(_outputDestination);
            }
            return ctx;
        }
        if (!window.__sharedAudioCtx) {
            window.__sharedAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
        }
        if (window.__sharedAudioCtx.state === 'suspended') {
            window.__sharedAudioCtx.resume();
        }
        if (!_gainNode) {
            _gainNode = window.__sharedAudioCtx.createGain();
            _gainNode.gain.value = 0.3;
            _outputDestination = _pendingOutputDestination || window.__sharedAudioCtx.destination;
            _gainNode.connect(_outputDestination);
        }
        return window.__sharedAudioCtx;
    };

    const SonicSF = {
        loadedFonts: _loadedFonts,

        // Route the shared FluidSynth output through a per-track node (e.g. the
        // track's gainNode) so the track's FX chain / fader / pan affect the
        // soundfont instrument. Pass null to restore the default master-bus route.
        setOutputDestination: function (node) {
            try {
                if (_gainNode) {
                    const dest = node || (window.masterBus ? window.masterBus.input : ((_audioCtx || window.__sharedAudioCtx).destination));
                    // DEDUPE: đích không đổi → KHÔNG disconnect/reconnect.
                    // Swap dư giữa dòng notes đang phát (applyAllTrackMuteSolo →
                    // updateSfRouting gọi lại cùng đích sfEntry sau noteon đầu)
                    // làm ScriptProcessor xuất buffer uninitialized → NaN →
                    // 11 biquad "state is bad" → CÂM (mọi log: state-bad nổ
                    // ngay sau setOutputDestination lần 2).
                    if (dest === _outputDestination) return;
                    _gainNode.disconnect();
                    _gainNode.connect(dest);
                    // Reconnect VU analyser tap (app.jsx VU tick gắn __vuAnalyser
                    // trên _gainNode): disconnect() không đối số ngắt MỌI kết
                    // nối kể cả analyser → sfAudioPeak = 0 mãi → track VU MIDI
                    // không nhảy khi có âm (user bug 06:35).
                    if (_gainNode.__vuAnalyser) _gainNode.connect(_gainNode.__vuAnalyser);
                    _outputDestination = dest;
                    console.log('[SonicSF] setOutputDestination to:', node ? 'track node (sfEntry)' : 'masterBus.input');
                } else {
                    _pendingOutputDestination = node || null;
                    console.log('[SonicSF] setOutputDestination pending:', node ? 'track node (sfEntry)' : 'null');
                }
            } catch (e) {
                console.warn('[SonicSF] setOutputDestination error:', e);
            }
        },
        getOutputNode: function () {
            return _gainNode;
        },

        init: async function (audioContext) {
            if (_initialized && _fluidModule) return;
            if (_initPromise) return _initPromise;
            _initPromise = (async () => {
                if (!window.__FluidSynthModuleFactory) {
                    console.log("[SonicSF] Waiting for FluidSynth WASM module to load...");
                    await window.__FluidSynthReady;
                    if (!window.__FluidSynthModuleFactory) {
                        console.warn("[SonicSF] FluidSynth WASM still not available after waiting.");
                        _initPromise = null;
                        return;
                    }
                }
                try {
                    _audioCtx = audioContext;
                    if (_audioCtx.state === 'suspended') await _audioCtx.resume();
                    // Create gain node for master bus routing before any node connections
                    if (!_gainNode) {
                        _gainNode = _audioCtx.createGain();
                        _gainNode.gain.value = 0.3;
                        _outputDestination = _pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination);
                        _gainNode.connect(_outputDestination);
                    }

                    console.log("[SonicSF] AudioCtx state:", _audioCtx.state, "sampleRate:", _audioCtx.sampleRate);

                    // ── Renderer selection ──
                    // ScriptProcessor is the default and FINAL choice: it is
                    // pull-based (onaudioprocess is invoked by the audio thread),
                    // so it cannot starve when the main thread is busy (font
                    // loading, WASM decode, UI). The AudioWorklet push model
                    // (setInterval on the main thread) starves under load and
                    // produces SILENCE on this machine — repeatedly confirmed.
                    // The deprecation console warning is purely cosmetic.
                    var _useScriptNode = true;
                    try {
                        await _audioCtx.audioWorklet.addModule('/static/js/worklets/fluidsynth-bridge.js?v=202608031240');
                        console.log("[SonicSF] Worklet registered OK (unused)");
                    } catch (e) {
                        console.warn("[SonicSF] Worklet reg failed:", e);
                    }

                    console.log("[SonicSF] Initializing FluidSynth WASM Engine...");
                    var TOTAL_MEMORY = 256 * 1024 * 1024;
                    _fluidModule = await window.__FluidSynthModuleFactory({
                        locateFile: function (path) {
                            if (path.endsWith('.wasm')) {
                                return window.__FluidSynthLocateWasm ? window.__FluidSynthLocateWasm() : path;
                            }
                            return path;
                        },
                        TOTAL_MEMORY: TOTAL_MEMORY,
                        printErr: function (msg) {
                            // "No preset found on channel" is FluidSynth's
                            // expected notice when a soundfont simply has no
                            // preset for a bank (e.g. bank 128 on a melodic-only
                            // font) — the note is just silent, not an error.
                            if (msg && (msg.indexOf('No preset found on channel') !== -1 || msg.indexOf('There is no preset with bank number') !== -1)) return;
                            console.warn('[FluidSynth:err]', msg);
                        }
                    });

                    if (!_fluidModule || !_fluidModule._new_fluid_settings) {
                        throw new Error("FluidSynth WASM module loaded but API missing");
                    }

                    _settingsPtr = _fluidModule._new_fluid_settings();
                    _fluidModule._fluid_settings_setnum(_settingsPtr, "synth.sample-rate", _audioCtx.sampleRate || 44100);
                    _fluidModule._fluid_settings_setnum(_settingsPtr, "synth.gain", 1.0);
                    _fluidModule._fluid_settings_setnum(_settingsPtr, "synth.polyphony", 256);
                    _fluidModule._fluid_settings_setint(_settingsPtr, "synth.verbose", 0);
                    _fluidModule._fluid_settings_setint(_settingsPtr, "synth.ladspa.active", 0);
                    _fluidModule._fluid_settings_setstr(_settingsPtr, "player.timing-source", "audio");
                    console.log("[SonicSF] FluidSynth settings configured");

                    _synthPtr = _fluidModule._new_fluid_synth(_settingsPtr);
                    if (!_synthPtr) throw new Error("Failed to create FluidSynth synthesizer");
                    _fluidModule._fluid_synth_set_gain(_synthPtr, 1.0);

                    try { _fluidModule.FS.mkdir('/soundfonts'); } catch (e) {}

                    _leftBufPtr = _fluidModule._malloc(RENDER_BLOCK * 4);
                    _rightBufPtr = _fluidModule._malloc(RENDER_BLOCK * 4);

                    if (!_useScriptNode) {
                        try {
                            // Force stereo output regardless of the device's
                            // channel count — FluidSynth renders stereo, and a
                            // mono output would crash the worklet (out[1] undefined).
                            _workletNode = new AudioWorkletNode(_audioCtx, 'fluidsynth-bridge', {
                                numberOfOutputs: 1,
                                outputChannelCount: [2],
                                channelCount: 2,
                                channelCountMode: 'explicit'
                            });
                            _workletNode.connect(_gainNode);
                            console.log("[SonicSF] AudioWorklet node connected via gain");
                            _startRenderLoop();
                        } catch (e) {
                            console.warn("[SonicSF] AudioWorkletNode failed:", e);
                            _useScriptNode = true;
                        }
                    }
                    if (_useScriptNode) {
                        var spBufSz = 2048;
                        var spn = _audioCtx.createScriptProcessor(spBufSz, 0, 2);
                        var lp = _fluidModule._malloc(spBufSz * 4);
                        var rp = _fluidModule._malloc(spBufSz * 4);
                        // Heap WASM có thể realloc khi load SoundFont lớn (SGM-V2.01
                        // ~300MB) → lp/rp DANGLE → đọc vùng nhớ đã free → NaN/garbage
                        // → master chain "state is bad" → CÂM + stuck. Theo dõi
                        // buffer + re-malloc khi đổi.
                        var _heapBufRef = _fluidModule.HEAPU8.buffer;
                        spn.onaudioprocess = function (e) {
                            var left = e.outputBuffer.getChannelData(0);
                            var right = e.outputBuffer.getChannelData(1);
                            var sz = left.length;
                            try {
                                if (_fluidModule.HEAPU8.buffer !== _heapBufRef) {
                                    try { _fluidModule._free(lp); _fluidModule._free(rp); } catch (er2) {}
                                    lp = _fluidModule._malloc(sz * 4);
                                    rp = _fluidModule._malloc(sz * 4);
                                    _heapBufRef = _fluidModule.HEAPU8.buffer;
                                }
                                _fluidModule._fluid_synth_write_float(_synthPtr, sz, lp, 0, 1, rp, 0, 1);
                                var hf = _fluidModule.HEAPF32;
                                var lpb = lp >> 2, rpb = rp >> 2;
                                for (var si = 0; si < sz; si++) {
                                    // NaN sweep: mẫu NaN/Inf → 0 (chain biquad
                                    // KHÔNG BAO GIỜ được nhận NaN → không state-bad).
                                    var L = hf[lpb + si], R = hf[rpb + si];
                                    left[si] = isFinite(L) ? L : 0;
                                    right[si] = isFinite(R) ? R : 0;
                                }
                            } catch (er) {}
                        };
                        spn.connect(_gainNode);
                        _workletNode = spn;
                        console.log("[SonicSF] ScriptProcessorNode connected via gain (buf:", spBufSz, ")");
                    }

                    _initialized = true;
                    console.log("[SonicSF] FluidSynth WASM Engine initialized.");
                } catch (e) {
                    console.error("[SonicSF] FluidSynth init failed:", e);
                    _initPromise = null;
                    _cleanupFluid();
                }
            })();
            return _initPromise;
        },

        _allocCStr: function (str) {
            var ptr = _fluidModule._malloc(str.length + 1);
            for (var i = 0; i < str.length; i++) {
                _fluidModule.HEAPU8[ptr + i] = str.charCodeAt(i);
            }
            _fluidModule.HEAPU8[ptr + str.length] = 0;
            return ptr;
        },

        _tryLoadSFL: function (buf, ext) {
            var fname = '/' + ext + '_' + (++_sfloadSeq) + '_' + Date.now();
            try { _fluidModule.FS.unlink(fname); } catch (e) {}
            _fluidModule.FS.writeFile(fname, new Uint8Array(buf));
            var cPath = this._allocCStr(fname);
            // reset_presets = 0: loading a NEW soundfont must NOT reset the
            // presets already selected on other channels. With 1, FluidSynth
            // re-points every channel to the new font's preset 0, so loading a
            // second instrument silently changes the first one's sound
            // (decay/loop envelope…).
            var handle = _fluidModule._fluid_synth_sfload(_synthPtr, cPath, 0);
            _fluidModule._free(cPath);
            try { _fluidModule.FS.unlink(fname); } catch (e) {}
            return handle;
        },

        loadSoundFont: async function (sfId) {
            if (!_initialized || !_fluidModule) return false;
            if (_currentSfId === sfId) return true;
            if (_sfHandleMap.has(sfId)) {
                _currentSfId = sfId;
                return true;
            }
            // Deduplicate concurrent loads: rapid key presses (or several armed
            // tracks) all call loadSoundFont for the same font before the first
            // load resolves. Without this, the same soundfont is sfload'd several
            // times (handles 1,2,3,4…) — wasting the 256MB WASM heap and stalling
            // notes until each load finishes (audible lag, then silence).
            if (!_loadPromises[sfId]) {
                _loadPromises[sfId] = this._doLoadSoundFont(sfId).then(function (ok) {
                    // Do NOT cache failures: a transient error (network hiccup,
                    // memory pressure) must not permanently kill the instrument —
                    // the next note retries the load and recovers.
                    if (!ok) delete _loadPromises[sfId];
                    return ok;
                });
            }
            return _loadPromises[sfId];
        },

        _doLoadSoundFont: async function (sfId) {
            try {
                // KHÔNG unload SF cũ khi sfload SF mới: unload làm handle cũ
                // thành rác trong khi channel state vẫn trỏ tới → program_select
                // bị skip (progAlreadySet) → noteon trên handle đã unload →
                // "Instrument not found ... substituted prog 0". Heap 256MB đủ
                // cho vài SF (SGM + latin = 2 handle — log OK). SF cũ khi cần
                // lại chỉ được sfload lại nếu map bị xóa (không xảy ra ở đây).
                var cache = window.SonicSFStorage;
                var buf = cache ? await cache.getBuffer(sfId) : null;
                if (buf) {
                    var cachedOk = this._tryLoadSFL(buf, '.sf3');
                    if (cachedOk === -1) cachedOk = this._tryLoadSFL(buf, '.sf2');
                    if (cachedOk !== -1) {
                        _sfHandleMap.set(sfId, cachedOk);
                        _currentSfId = sfId;
                        _loadedFonts[sfId] = true;
                        _instrumentEpoch++;
                        console.log("[SonicSF] SoundFont loaded from cache:", sfId, "handle:", cachedOk);
                        return true;
                    }
                    // Stale/corrupt cache (e.g. old SF3 buffers the WASM can't
                    // decode) — drop it and re-download from the server.
                    console.warn("[SonicSF] Cached SoundFont unplayable, re-downloading:", sfId);
                    try { await cache.saveBuffer(sfId, null); } catch (e2) {}
                }
                var url = "/api/v1/plugins/soundfonts/download/" + encodeURIComponent(sfId) + "?t=" + Date.now();
                var resp = await fetch(url);
                if (!resp.ok) {
                    // Fallback: font bundled theo deployment (static/soundfonts —
                    // serve qua /soundfonts/{f} — catalog default-soundfonts).
                    var url2 = "/soundfonts/" + encodeURIComponent(sfId.replace(/^sf_/, '')) + "?t=" + Date.now();
                    var resp2 = await fetch(url2);
                    if (!resp2.ok) {
                        console.warn("[SonicSF] SoundFont not found:", sfId);
                        return false;
                    }
                    resp = resp2;
                }
                buf = await resp.arrayBuffer();
                if (cache) await cache.saveBuffer(sfId, buf);
                var sfHandle = this._tryLoadSFL(buf, '.sf3');
                if (sfHandle === -1) {
                    console.warn("[SonicSF] sfload .sf3 failed, trying .sf2 for", sfId);
                    sfHandle = this._tryLoadSFL(buf, '.sf2');
                }
                if (sfHandle === -1) {
                    console.error("[SonicSF] FluidSynth failed to parse SoundFont:", sfId);
                    return false;
                }
                _sfHandleMap.set(sfId, sfHandle);
                _currentSfId = sfId;
                _loadedFonts[sfId] = true;
                _instrumentEpoch++;
                console.log("[SonicSF] SoundFont loaded:", sfId, "handle:", sfHandle);
                return true;
            } catch (e) {
                console.warn("[SonicSF] loadSoundFont error:", e);
                return false;
            }
        },
        selectInstrument: async function (channel, bank, program, sfId) {
            if (!_initialized || !_fluidModule) {
                var ctx = (typeof getAudioContext === 'function') ? getAudioContext() : new (window.AudioContext || window.webkitAudioContext)();
                await this.init(ctx);
            }
            if (sfId) {
                var ok = await this.loadSoundFont(sfId);
                if (!ok) return;
            }
            var engKey = (sfId || '') + ':' + bank + ':' + program;
            if (channel === undefined || channel === null) {
                if (!_engineChMap[engKey]) {
                    var allocCh = this.allocateChannel(bank);
                    _engineChMap[engKey] = allocCh;
                    channel = allocCh;
                } else {
                    channel = _engineChMap[engKey];
                }
            } else if (!_engineChMap[engKey]) {
                _engineChMap[engKey] = channel;
            }
            var sfHandle = _sfHandleMap.get(sfId);
            // ⚠️ FIX âm sai instrument: tăng epoch → channel cache cũ thành
            // stale → note kế tiếp LUÔN program_select lại (hết "chọn Synth
            // String nghe Piano" do cache cũ skip).
            _instrumentEpoch++;
            if (sfHandle !== undefined) {
                try {
                    var _sr = _fluidModule._fluid_synth_program_select(_synthPtr, channel, sfHandle, bank, program);
                    if (_sr !== 0) {
                        // Preset không tồn tại ở bank/program này → thử bank 0
                        try { _fluidModule._fluid_synth_program_select(_synthPtr, channel, sfHandle, 0, program); } catch (e2) {}
                    }
                } catch (e) {}
            } else {
                try { _fluidModule._fluid_synth_bank_select(_synthPtr, channel, bank); } catch (e) {}
                try { _fluidModule._fluid_synth_program_change(_synthPtr, channel, program); } catch (e) {}
            }
            _channels[channel].bank = bank;
            _channels[channel].program = program;
            _channels[channel].isPercussion = (bank === 128);
            _channels[channel].sfId = sfId;
            _channels[channel]._epoch = _instrumentEpoch;
        },

        controllerChange: function (channel, controller, value) {
            if (channel < 0 || channel > 15) return;
            if (_initialized && _fluidModule) {
                try {
                    _fluidModule._fluid_synth_cc(_synthPtr, channel, controller, value);
                } catch (e) {}
            }
            if (controller === 0) {
                _channels[channel].bank = value;
                _channels[channel].isPercussion = (value === 128);
            }
            if (controller === 64) {
                _sustainStates[channel] = value >= 64;
            }
        },

        programChange: function (channel, program) {
            if (channel < 0 || channel > 15) return;
            if (_initialized && _fluidModule) {
                try {
                    _fluidModule._fluid_synth_program_change(_synthPtr, channel, program);
                } catch (e) {}
            }
            _channels[channel].program = program;
        },

        allocateChannel: function (bank) {
            if (bank === 128) return 9;
            var ch = _nextMelodicChannel % 9;
            _nextMelodicChannel = (_nextMelodicChannel + 1) % 9;
            return ch;
        },

        applyAITrackInstrument: function (bank, program, synthEngine) {
            if (synthEngine) {
                bank = bank !== undefined ? bank : (synthEngine.soundfont_bank || 0);
                program = program !== undefined ? program : (synthEngine.soundfont_program || 0);
            }
            var sfId = synthEngine && synthEngine.soundfont_id;
            var engKey = (sfId || '') + ':' + bank + ':' + program;
            if (!_engineChMap[engKey]) {
                var channel = this.allocateChannel(bank);
                _engineChMap[engKey] = channel;
            }
            this.selectInstrument(_engineChMap[engKey], bank, program, sfId);
            return _engineChMap[engKey];
        },

        getChannelState: function (channel) {
            if (channel < 0 || channel > 15) return null;
            return { bank: _channels[channel].bank, program: _channels[channel].program, isPercussion: _channels[channel].isPercussion };
        },

        sustainActive: function (channel) {
            if (channel < 0 || channel > 15) return false;
            return _sustainStates[channel];
        },

        pitchBend: function (channel, value) {
            if (channel < 0 || channel > 15) return;
            if (_initialized && _fluidModule) {
                try {
                    _fluidModule._fluid_synth_pitch_bend(_synthPtr, channel, value);
                } catch (e) {}
            }
        },

        stopNote: function (channel, pitch) {
            if (channel < 0 || channel > 15) return;
            var key = channel + ':' + pitch;
            // Hủy note đang CHỜ LOAD soundfont (chưa noteon) — trước đây note
            // này vẫn bắn TRỄ sau khi thả phím → âm loop không dừng được.
            if (_pendingNoteOns[key]) delete _pendingNoteOns[key];
            // ⚠️ FIX: dừng fallback oscillator theo 'channel:pitch' — trước đây
            // stopNote chỉ noteoff FluidSynth, KHÔNG dừng oscillator (khi WASM
            // chưa sẵn sàng / font load fail → _playNoteFallback) → âm kêu liên
            // tục mặc dù đã thả phím (không xử lí noteoff).
            var _oscIds = _activeOscillatorsByKey[key];
            if (_oscIds && _oscIds.length) {
                var _nowT = getCtx().currentTime;
                _oscIds.slice().forEach(function (oid) {
                    var entry = _activeOscillators[oid];
                    if (entry) {
                        try { entry.gain.gain.cancelScheduledValues(_nowT); entry.gain.gain.setValueAtTime(0, _nowT); } catch (e) {}
                        try { entry.osc.stop(_nowT); } catch (e) {}
                        delete _activeOscillators[oid];
                    }
                });
                delete _activeOscillatorsByKey[key];
            }
            if (_initialized && _fluidModule) {
                var mappedChs = _activeNotes[key];
                if (mappedChs === undefined) mappedChs = [channel];
                for (var i = 0; i < mappedChs.length; i++) {
                    try { _fluidModule._fluid_synth_noteoff(_synthPtr, mappedChs[i], pitch); } catch (e) {}
                }
                delete _activeNotes[key];
            }
        },

        playNote: function (note, velocity, durationMs, startTime, program, destinationNode, channel, synthEngine) {
            if (!_initialized || !_fluidModule) {
                this._lazyInit();
                this._playNoteFallback(note, velocity, 2000, startTime, program, destinationNode, channel, synthEngine);
                return;
            }
            this._playNoteFluid(note, velocity, durationMs, startTime, program, channel, synthEngine);
        },

        _lazyInit: async function () {
            if (_initialized && _fluidModule) return;
            if (_initPromise) return;
            try {
                var ctx = (typeof getAudioContext === 'function') ? getAudioContext() : new (window.AudioContext || window.webkitAudioContext)();
                if (ctx.state === 'suspended') await ctx.resume();
                await this.init(ctx);
            } catch (e) {}
        },

        _playNoteFluid: function (note, velocity, durationMs, startTime, program, channel, synthEngine) {
            var parsedPitch = parseInt(note);
            var midiPitch = isNaN(parsedPitch) ? 60 : Math.min(127, Math.max(0, parsedPitch));
            var rawVel = (typeof velocity === 'number' && isFinite(velocity)) ? (velocity > 1 ? velocity : velocity * 127) : 100;
            if (isNaN(rawVel)) rawVel = 100;
            var midiVel = Math.min(127, Math.max(1, Math.floor(rawVel)));
            var _origChannel = channel;
            var usedBank = 0, usedProg = 0;
            if (synthEngine) {
                usedBank = synthEngine.soundfont_bank || 0;
                usedProg = synthEngine.soundfont_program || 0;
                if (channel === undefined) {
                    var engKey = (synthEngine.soundfont_id || '') + ':' + usedBank + ':' + usedProg;
                    var mappedCh = _engineChMap[engKey];
                    if (mappedCh !== undefined) {
                        channel = mappedCh;
                    } else {
                        channel = usedBank === 128 ? 9 : 0;
                    }
                }
                if (channel === undefined) channel = (usedBank === 128 ? 9 : 0);
            } else if (program !== undefined) {
                usedProg = program;
                if (channel === undefined) channel = 0;
            } else {
                // No instrument configured: silent — no FluidSynth, no oscillator.
                return;
            }
            if (channel === undefined) channel = (usedBank === 128 ? 9 : 0);
            var ch = channel;
            var ctx = getCtx();
            var now = ctx.currentTime;
            var delay = (typeof startTime === 'number' && startTime > now) ? (startTime - now) : 0;
            var durSec = (durationMs || 500) / 1000;
            var scheduledNote = { on: null, off: null };
            var self = this;
            var doNote = function () {
                try {
                    var finalBank = parseInt(usedBank);
                    if (isNaN(finalBank) || !isFinite(finalBank)) finalBank = 0;
                    var finalProg = parseInt(usedProg);
                    if (isNaN(finalProg) || !isFinite(finalProg)) finalProg = 0;
                    var finalSfId = synthEngine ? synthEngine.soundfont_id : undefined;
                    var cachedCh = _channels[ch];
                    // The note's own synth engine (track instrument) is
                    // authoritative. Channel state is only a cache: it must never
                    // mask the track's instrument, otherwise multi-track ARM or a
                    // re-picked instrument plays the wrong soundfont. Without an
                    // engine, fall back to the soundfont configured on the channel.
                    if (!synthEngine && cachedCh && cachedCh.sfId !== undefined) {
                        finalBank = parseInt(cachedCh.bank) || 0;
                        finalProg = parseInt(cachedCh.program) || 0;
                        finalSfId = cachedCh.sfId;
                    }
                    // Ensure the soundfont is actually loaded before the note plays.
                    // Quick instrument pick on a track does not pre-load it, so load
                    // lazily here and retry the note once the font is ready.
                    if (finalSfId && !_sfHandleMap.has(finalSfId)) {
                        // Cooldown lỗi: font 404 → KHÔNG spam fetch mỗi note (10s)
                        // — note chạy thẳng fallback để CÓ ÂM.
                        var _lastFail = _sfLoadFailAt[finalSfId] || 0;
                        if (Date.now() - _lastFail < 10000) {
                            try { self._playNoteFallback(note, velocity, durationMs, startTime, program, null, channel, synthEngine); } catch (e) {}
                            return;
                        }
                        console.log('[SonicSF] soundfont not loaded yet, loading:', finalSfId);
                        // Đăng ký note CHỜ LOAD trước khi load — để stopNote (thả
                        // phím) / stopAll / panic hủy được note này. Trước đây
                        // note deferred vẫn bắn TRỄ sau khi thả phím hoặc sau
                        // Stop → âm loop không dừng với preview MIDI Keyboard.
                        var _pendKey = ch + ':' + midiPitch;
                        var _gen = _noteGeneration;
                        _pendingNoteOns[_pendKey] = (_pendingNoteOns[_pendKey] || 0) + 1;
                        self.loadSoundFont(finalSfId).then(function (ok) {
                            // stopAll/panic chạy trong lúc load → generation đổi → bỏ
                            if (_gen !== _noteGeneration) return;
                            // stopNote (thả phím) đã hủy → KHÔNG bắn note trễ nữa
                            var _pend = _pendingNoteOns[_pendKey];
                            if (_pend) {
                                _pendingNoteOns[_pendKey] = _pend - 1;
                                if (_pendingNoteOns[_pendKey] <= 0) delete _pendingNoteOns[_pendKey];
                            } else {
                                return;
                            }
                            console.log('[SonicSF] loadSoundFont result:', ok, 'for:', finalSfId);
                            if (ok) {
                                doNote();
                            } else {
                                // Font KHÔNG tải được (404/format) → KHÔNG drop note
                                // câm lặng ("bỏ qua WASM") — fallback oscillator.
                                _sfLoadFailAt[finalSfId] = Date.now();
                                try { self._playNoteFallback(note, velocity, durationMs, startTime, program, null, channel, synthEngine); } catch (e) {}
                            }
                        });
                        return;
                    }
                    // Program change at note time, not call time — ensures correct
                    // instrument for each item regardless of processing order.
                    // Skip if the channel already has this exact instrument (avoids
                    // per-note soundfont reloads that cause audible crackle/glitches).
                    // ⚠️ Chỉ skip khi handle SF vẫn CÒN HỢP LỆ trong map — nếu
                    // không → vẫn program_select lại (tránh dùng handle đã unload).
                    var progAlreadySet = cachedCh && cachedCh.program === finalProg && cachedCh.bank === finalBank && cachedCh.sfId === finalSfId
                        && cachedCh._epoch === _instrumentEpoch
                        && (finalSfId ? _sfHandleMap.has(finalSfId) : true);
                    if ((synthEngine || program !== undefined) && !progAlreadySet) {
                        var sfHandle = finalSfId ? _sfHandleMap.get(finalSfId) : undefined;
                        console.log('[SonicSF] selectProgram for channel:', ch, 'sfHandle:', sfHandle, 'bank:', finalBank, 'prog:', finalProg);
                        if (sfHandle !== undefined) {
                            try {
                                // Percussion (bank 128): tìm preset HỢP LỆ trong
                                // font — quét bank 128 + bank 0 (0-127) MỘT LẦN,
                                // cache theo sfId. Trước đây chỉ thử 4 preset cố
                                // định → font không có → cache channel = (128,0)
                                // INVALID → note sau skip re-select (progAlreadySet)
                                // → noteon preset rỗng = CÂM ("1 âm đầu rồi câm").
                                if (finalBank === 128) {
                                    var _vKey = finalSfId || ('h' + sfHandle);
                                    if (_validPercCache[_vKey] === undefined) {
                                        var _found = null;
                                        for (var _b = 0; _b < 2 && !_found; _b++) {
                                            var _bk = _b === 0 ? 128 : 0;
                                            for (var _p = 0; _p < 128 && !_found; _p++) {
                                                try {
                                                    if (_fluidModule._fluid_synth_program_select(_synthPtr, 9, sfHandle, _bk, _p) === 0) {
                                                        _found = [_bk, _p];
                                                    }
                                                } catch (e) {}
                                            }
                                        }
                                        _validPercCache[_vKey] = _found;
                                    }
                                    if (_validPercCache[_vKey]) {
                                        finalBank = _validPercCache[_vKey][0];
                                        finalProg = _validPercCache[_vKey][1];
                                    }
                                }
                                var _selRet = _fluidModule._fluid_synth_program_select(_synthPtr, ch, sfHandle, finalBank, finalProg);
                                if (_selRet !== 0 && finalBank !== 0) {
                                    // Preset không tồn tại ở bank này → thử bank 0
                                    // (hết "noteon preset rỗng = âm sai/không đúng")
                                    try { _fluidModule._fluid_synth_program_select(_synthPtr, ch, sfHandle, 0, finalProg); } catch (e2) {}
                                }
                            } catch (e) {}
                        } else {
                            try { _fluidModule._fluid_synth_bank_select(_synthPtr, ch, finalBank); } catch (e) {}
                            try { _fluidModule._fluid_synth_program_change(_synthPtr, ch, finalProg); } catch (e) {}
                        }
                        if (!_channels[ch]) _channels[ch] = {};
                        _channels[ch].bank = finalBank;
                        _channels[ch].program = finalProg;
                        _channels[ch].sfId = finalSfId;
                        _channels[ch]._epoch = _instrumentEpoch;
                    }
                    console.log('[SonicSF] noteon channel:', ch, 'pitch:', midiPitch, 'vel:', midiVel, 'sfId:', finalSfId);
                    _fluidModule._fluid_synth_noteon(_synthPtr, ch, midiPitch, midiVel);
                    var noteMapKey = (_origChannel !== undefined ? _origChannel : 0) + ':' + midiPitch;
                    if (!_activeNotes[noteMapKey]) _activeNotes[noteMapKey] = [];
                    if (_activeNotes[noteMapKey].indexOf(ch) === -1) _activeNotes[noteMapKey].push(ch);
                    if (durationMs > 0 && durationMs < 60000) {
                        scheduledNote.off = setTimeout(function () {
                            try {
                                _fluidModule._fluid_synth_noteoff(_synthPtr, ch, midiPitch);
                                var arr = _activeNotes[noteMapKey];
                                if (arr) {
                                    var idx = arr.indexOf(ch);
                                    if (idx >= 0) arr.splice(idx, 1);
                                    if (arr.length === 0) delete _activeNotes[noteMapKey];
                                }
                            } catch (e) {}
                        }, durationMs);
                    }
                } catch (e) {
                    console.warn("[SonicSF] FluidSynth noteOn error:", e);
                    self._playNoteFallback(note, velocity, durationMs, startTime, program, null, channel, synthEngine);
                }
            };
            if (delay > 0) {
                scheduledNote.on = setTimeout(doNote, delay * 1000);
                _scheduledNotes.push(scheduledNote);
            } else {
                doNote();
            }
        },

        // Hủy mọi note-on được schedule (tương lai) + note-off mọi notes đang
        // ngân — gọi khi STOP/PAUSE để hết "âm thanh bị stuck" (note-on chưa
        // bắn vẫn bắn sau khi dừng; notes durationMs>=60000 không có note-off
        // tự động → ngân vô hạn → VU master nhảy dù không play).
        panic: function () {
            _scheduledNotes.forEach(function (sn) { if (sn.on) { clearTimeout(sn.on); sn.on = null; } });
            _scheduledNotes = [];
            // Hủy mọi note đang CHỜ LOAD soundfont (deferred) — note cũ thành no-op
            _noteGeneration++;
            _pendingNoteOns = {};
            if (_initialized && _fluidModule) {
                // noteoff TỪNG note đang ngân (binding _fluid_synth_noteoff chắc
                // chắn tồn tại — đã dùng cho duration hết) — all_notes_off có
                // thể không có trong WASM exports (catch nuốt → notes kẹt).
                Object.keys(_activeNotes).forEach(function (key) {
                    var parts = key.split(':');
                    var pitch = parseInt(parts[1], 10);
                    (_activeNotes[key] || []).forEach(function (ch) {
                        try { _fluidModule._fluid_synth_noteoff(_synthPtr, ch, pitch); } catch (e) {}
                    });
                });
                try {
                    for (var c = 0; c < 16; c++) _fluidModule._fluid_synth_all_notes_off(_synthPtr, c);
                } catch (e) {}
            }
            // Dừng fallback oscillator (FluidSynth chưa sẵn sàng / font fail)
            try {
                var _pctx = getCtx();
                var _pnow = _pctx.currentTime;
                Object.values(_activeOscillators).forEach(function (entry) {
                    try { if (entry.gain) { entry.gain.gain.cancelScheduledValues(_pnow); entry.gain.gain.setValueAtTime(0, _pnow); } } catch (e) {}
                    try { if (entry.osc) entry.osc.stop(_pnow); } catch (e) {}
                });
                Object.keys(_activeOscillators).forEach(function (k) { delete _activeOscillators[k]; });
                _activeOscillatorsByKey = {};
            } catch (e) {}
            _activeNotes = {};
        },

        _playNoteFallback: function (note, velocity, durationMs, startTime, program, destinationNode, channel, synthEngine) {
            var ctx = getCtx();
            var freq = 440 * Math.pow(2, (note - 69) / 12);
            if (freq <= 0 || isNaN(freq)) return null;
            if (synthEngine) {
                var ch = channel !== undefined ? channel : (synthEngine.soundfont_bank === 128 ? 9 : 0);
                this.controllerChange(ch, 0, synthEngine.soundfont_bank || 0);
                this.programChange(ch, synthEngine.soundfont_program || 0);
                if (channel === undefined) channel = ch;
                if (program === undefined) program = synthEngine.soundfont_program;
            }
            var osc = ctx.createOscillator();
            var noteGain = ctx.createGain();
            var oscType = 'triangle';
            var attackTime = 0.03, decayTime = 0.1, sustainLevel = 0.5, releaseTime = 0.2, volFactor = 0.25;
            var prog = program !== undefined ? parseInt(program) : 0;
            // CHỈ dùng cache channel khi KHÔNG có program/synthEngine được
            // truyền — trước đây override program của track bằng cache channel
            // (bị track khác cùng channel ghi đè → preview note vẽ mới mang
            // nhạc cụ của track TRƯỚC).
            if (program === undefined && channel !== undefined && channel >= 0 && channel < 16) {
                prog = _channels[channel].program || prog;
            }
            if (prog >= 0 && prog <= 7) { oscType = 'sine'; decayTime = 0.3; sustainLevel = 0.1; releaseTime = 0.2; }
            else if (prog >= 8 && prog <= 15) { oscType = 'sine'; decayTime = 0.1; sustainLevel = 0.0; releaseTime = 0.1; }
            else if (prog >= 16 && prog <= 23) { oscType = 'sine'; attackTime = 0.05; sustainLevel = 0.8; releaseTime = 0.1; }
            else if (prog >= 24 && prog <= 31) { oscType = 'triangle'; decayTime = 0.4; sustainLevel = 0.2; releaseTime = 0.3; }
            else if (prog >= 32 && prog <= 39) { oscType = 'triangle'; attackTime = 0.02; decayTime = 0.2; sustainLevel = 0.6; releaseTime = 0.2; }
            else if (prog >= 40 && prog <= 47) { oscType = 'sawtooth'; attackTime = 0.15; sustainLevel = 0.8; releaseTime = 0.5; volFactor = 0.15; }
            else if (prog >= 48 && prog <= 55) { oscType = 'sawtooth'; attackTime = 0.2; sustainLevel = 0.8; releaseTime = 0.6; volFactor = 0.12; }
            else if (prog >= 56 && prog <= 63) { oscType = 'sawtooth'; attackTime = 0.08; sustainLevel = 0.7; releaseTime = 0.3; volFactor = 0.15; }
            else if (prog >= 64 && prog <= 71) { oscType = 'square'; attackTime = 0.05; sustainLevel = 0.6; releaseTime = 0.2; volFactor = 0.15; }
            else if (prog >= 72 && prog <= 79) { oscType = 'sine'; attackTime = 0.1; sustainLevel = 0.7; releaseTime = 0.3; volFactor = 0.2; }
            else if (prog >= 80 && prog <= 119) { oscType = 'sawtooth'; attackTime = 0.05; sustainLevel = 0.6; releaseTime = 0.4; volFactor = 0.15; }
            osc.type = oscType;
            osc.frequency.setValueAtTime(freq, 0);
            var startAt = startTime !== undefined ? startTime : ctx.currentTime;
            var durSec = durationMs / 1000;
            var vel = (typeof velocity === 'number' && isFinite(velocity) && !isNaN(velocity)) ? (velocity > 1 ? velocity / 127 : velocity) : 0.8;
            if (isNaN(vel)) vel = 0.8;
            var targetGain = vel * volFactor;
            noteGain.gain.setValueAtTime(0, startAt);
            noteGain.gain.linearRampToValueAtTime(targetGain, startAt + attackTime);
            noteGain.gain.linearRampToValueAtTime(targetGain * sustainLevel, startAt + attackTime + decayTime);
            var releaseStart = startAt + Math.max(attackTime + decayTime, durSec);
            noteGain.gain.linearRampToValueAtTime(targetGain * sustainLevel, releaseStart);
            noteGain.gain.linearRampToValueAtTime(0, releaseStart + releaseTime);
            osc.connect(noteGain);
            noteGain.connect(destinationNode || _gainNode || ctx.destination);
            osc.start(startAt);
            var stopAt = releaseStart + releaseTime + 0.02;
            osc.stop(stopAt);
            var oscId = note + '_' + Date.now() + '_' + Math.random();
            _activeOscillators[oscId] = { osc: osc, gain: noteGain };
            // Đăng ký theo 'channel:pitch' để stopNote dừng được khi thả phím
            var oscKey = (channel !== undefined && channel >= 0 && channel < 16) ? (channel + ':' + note) : null;
            if (oscKey) {
                if (!_activeOscillatorsByKey[oscKey]) _activeOscillatorsByKey[oscKey] = [];
                _activeOscillatorsByKey[oscKey].push(oscId);
            }
            setTimeout(function () {
                delete _activeOscillators[oscId];
                if (oscKey && _activeOscillatorsByKey[oscKey]) {
                    var _oi = _activeOscillatorsByKey[oscKey].indexOf(oscId);
                    if (_oi >= 0) _activeOscillatorsByKey[oscKey].splice(_oi, 1);
                    if (_activeOscillatorsByKey[oscKey].length === 0) delete _activeOscillatorsByKey[oscKey];
                }
            }, (stopAt - ctx.currentTime) * 1000 + 100);
            return osc;
        },

        stopAll: function () {
            // Hủy mọi note đang CHỜ LOAD soundfont (deferred) — note cũ thành
            // no-op sau Stop (âm loop không dừng khi preview MIDI Keyboard).
            _noteGeneration++;
            _pendingNoteOns = {};
            if (_initialized && _fluidModule) {
                // noteoff từng note đang ngân (binding chắc chắn tồn tại) —
                // phòng all_notes_off không có trong WASM exports.
                Object.keys(_activeNotes).forEach(function (key) {
                    var parts = key.split(':');
                    var pitch = parseInt(parts[1], 10);
                    (_activeNotes[key] || []).forEach(function (ch) {
                        try { _fluidModule._fluid_synth_noteoff(_synthPtr, ch, pitch); } catch (e) {}
                    });
                });
                for (var ch = 0; ch < 16; ch++) {
                    try { _fluidModule._fluid_synth_all_notes_off(_synthPtr, ch); } catch (e) {}
                }
            }
            while (_scheduledNotes.length > 0) {
                var n = _scheduledNotes.pop();
                if (n.on) { clearTimeout(n.on); n.on = null; }
                if (n.off) { clearTimeout(n.off); n.off = null; }
            }
            var ctx = getCtx();
            var now = ctx.currentTime;
            Object.values(_activeOscillators).forEach(function (entry) {
                try {
                    if (entry.gain) { entry.gain.gain.cancelScheduledValues(now); entry.gain.gain.setValueAtTime(0, now); }
                    if (entry.osc) try { entry.osc.stop(now); } catch (e) {}
                } catch (e) {}
            });
            Object.keys(_activeOscillators).forEach(function (k) { delete _activeOscillators[k]; });
            _activeOscillatorsByKey = {};
            _activeNotes = {};
        },

        saveToIndexedDB: async function (name, arrayBuffer) {
            if (window.SonicStorage && window.SonicStorage.saveToIndexedDB) {
                await window.SonicStorage.saveToIndexedDB('soundfont_' + name, arrayBuffer);
            }
        },

        loadFromIndexedDB: async function (name) {
            if (window.SonicStorage && window.SonicStorage.loadFromIndexedDB) {
                return await window.SonicStorage.loadFromIndexedDB('soundfont_' + name);
            }
            return null;
        }
    };

    function _startRenderLoop() {
        if (_renderTimer) return;
        var Module = _fluidModule;
        var synth = _synthPtr;
        var node = _workletNode;
        var leftPtr = _leftBufPtr;
        var rightPtr = _rightBufPtr;
        var block = RENDER_BLOCK;
        var maxQueue = QUEUE_TARGET;
        var queueDepth = 0;
        var lastTick = performance.now();
        var frameMs = (block / _audioCtx.sampleRate) * 1000;

        // Track consumption by wall-clock time instead of async messages — immune
        // to message-latency races that could underrun (silence gaps → crackle).
        function pushFrame() {
            if (!Module || !synth || !node) return;
            try {
                var lpb = leftPtr >> 2;
                var rpb = rightPtr >> 2;
                Module.HEAPF32.fill(0, lpb, lpb + block);
                Module.HEAPF32.fill(0, rpb, rpb + block);
                Module._fluid_synth_write_float(synth, block, leftPtr, 0, 1, rightPtr, 0, 1);
                var leftArr = new Float32Array(Module.HEAPF32.subarray(lpb, lpb + block));
                var rightArr = new Float32Array(Module.HEAPF32.subarray(rpb, rpb + block));
                node.port.postMessage({ type: 'PCM', L: leftArr, R: rightArr }, [leftArr.buffer, rightArr.buffer]);
                queueDepth++;
            } catch (e) { console.warn("[SonicSF] pushFrame error:", e); }
        }

        function fillLoop() {
            if (!Module || !synth || !node || !_initialized) {
                _renderTimer = null;
                return;
            }
            var now = performance.now();
            queueDepth = Math.max(0, queueDepth - (now - lastTick) / frameMs);
            lastTick = now;
            var needed = Math.min(maxQueue - queueDepth, maxQueue);
            for (var i = 0; i < needed; i++) {
                pushFrame();
            }
        }

        _renderTimer = setInterval(fillLoop, Math.max(4, frameMs * 0.5));
    }

    function _stopRenderLoop() {
        if (_renderTimer) {
            clearInterval(_renderTimer);
            _renderTimer = null;
        }
    }

    function _cleanupFluid() {
        _stopRenderLoop();
        if (_leftBufPtr && _fluidModule) { try { _fluidModule._free(_leftBufPtr); } catch (e) {} _leftBufPtr = null; }
        if (_rightBufPtr && _fluidModule) { try { _fluidModule._free(_rightBufPtr); } catch (e) {} _rightBufPtr = null; }
        if (_synthPtr && _fluidModule) { try { _fluidModule._delete_fluid_synth(_synthPtr); } catch (e) {} _synthPtr = null; }
        if (_settingsPtr && _fluidModule) { try { _fluidModule._delete_fluid_settings(_settingsPtr); } catch (e) {} _settingsPtr = null; }
        _fluidModule = null;
        _initialized = false;
    }

    window.SonicSF = SonicSF;
})();
