# -*- coding: utf-8 -*- """Patch app/static/js/app.jsx: thay render per-item (scheduleNativeSfItem / scheduleNativeVstItem) bằng scheduleNativeTrack (render cả track 1 request) + epoch guard + decode queue + prerenderNativeTracks. Debug tool — không commit.""" import io, sys sys.stdout.reconfigure(encoding="utf-8") PATH = "app/static/js/app.jsx" with open(PATH, "rb") as f: raw = f.read() src = raw.decode("utf-8").replace("\r\n", "\n") orig = src def save(): with open(PATH, "wb") as f: f.write(src.replace("\n", "\r\n").encode("utf-8")) # ── Patch 1: thay scheduleNativeSfItem (per-item) bằng họ scheduleNativeTrack ── if "_nativePlayEpoch" not in src: OLD_START = "// \u2500\u2500 Native soundfont item render (standalone, transport) \u2500" OLD_END = " } catch (e) { console.warn('[NativeSF] scheduleNativeSfItem error:', e); }\n};" i0 = src.index(OLD_START) i1 = src.index(OLD_END) + len(OLD_END) assert src[i1:i1+4] == "\n\n//", repr(src[i1:i1+40]) NEW = """// \u2500\u2500 Native track render (standalone, transport) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 // Render TO\u00c0N B\u1ed8 MIDI items c\u1ee7a track b\u1eb1ng 1 request (native FluidSynth cho // soundfont, native_bridge --render cho VSTi) \u2192 decode \u2192 1 BufferSource duy // nh\u1ea5t cho c\u1ea3 track (nh\u01b0 audio clip). Thay th\u1ebf render per-item: RENDER_LOCK // server serialize t\u1eebng item \u2192 project nhi\u1ec1u item = delay v\u00e0i ch\u1ee5c gi\u00e2y. // opts: // baseOffsetSec: offset th\u00eam (section: secStart) | limitSec: ch\u1eb7n t\u1ea1i secEnd // isActive(): guard stop gi\u1eefa ch\u1eebng | sources: m\u1ea3ng ngu\u1ed3n \u0111\u1ec3 stop // playheadNow(): playhead TH\u1eacT t\u1ea1i th\u1eddi \u0111i\u1ec3m resolve \u2014 render tr\u1ec5 v\u00e0i ch\u1ee5c // gi\u00e2y \u2192 item v\u1eabn schedule \u0111\u00fang v\u1ecb tr\u00ed playhead hi\u1ec7n t\u1ea1i (kh\u00f4ng ch\u1ed3ng // \u00e2m/wall-of-sound nh\u01b0 d\u00f9ng offsetTime c\u0169). // notesOverride: d\u00f9ng thay track.midiItems (piano roll tab). // Epoch guard: stop/rebuild t\u0103ng _nativePlayEpoch \u2192 m\u1ecdi render pending c\u1ee7a // l\u1ea7n play c\u0169 resolve nh\u01b0ng b\u1ecb skip (kh\u00f4ng duplicate schedule). let _nativePlayEpoch = 0; const _nativeTrackUrls = {}; const _nativeTrackPending = {}; const _buildNativeTrackNotes = (track, bpm, baseOffsetSec, notesOverride) => { const secPerBeat = 60.0 / (parseInt(bpm) || 120); const items = notesOverride ? [{ startTime: 0, notes: notesOverride }] : ((track && track.midiItems) || []); let trackStartAbs = Infinity; const absNotes = []; items.forEach(function (item) { const itemStartAbs = (baseOffsetSec || 0) + (item.startTime || 0); if (itemStartAbs < trackStartAbs) trackStartAbs = itemStartAbs; (item.notes || []).forEach(function (n) { absNotes.push({ pitch: n.pitch || 60, start_beat: itemStartAbs / secPerBeat + (n.start_beat || 0), duration_beats: n.duration_beats || 1, velocity: n.velocity != null ? n.velocity : 0.8, }); }); }); return { absNotes: absNotes, trackStartAbs: trackStartAbs }; }; const _nativeTrackKey = (track, bpm, baseOffsetSec, notesOverride) => { try { const eng = track && track.synth_engine; const instrumentId = (eng && eng.plugin_id) || (track && track.instrumentId) || ''; const sfId = (eng && eng.soundfont_id) || ''; const data = _buildNativeTrackNotes(track, bpm, baseOffsetSec, notesOverride); const nk = data.absNotes.map(function (n) { return n.pitch + ':' + n.start_beat + ':' + n.duration_beats + ':' + n.velocity; }).join(','); return [track && track.id ? track.id : '', instrumentId, sfId, (eng && eng.preset_id) || '', (eng && eng.preset_path) || '', parseFloat(bpm) || 120, baseOffsetSec || 0, nk].join('|'); } catch (e) { return ''; } }; const _renderNativeTrack = (track, bpm, baseOffsetSec, notesOverride) => { try { const eng = track && track.synth_engine; const data = _buildNativeTrackNotes(track, bpm, baseOffsetSec, notesOverride); if (!data.absNotes.length) return Promise.resolve(null); if (isSfTrackEngine(eng)) { const sfId = eng && eng.soundfont_id; if (!sfId) return Promise.resolve(null); return window.SonicAPI.soundfontRender({ soundfont_id: sfId, bank: (eng && eng.soundfont_bank) || 0, program: (eng && eng.soundfont_program) || 0, bpm: parseFloat(bpm) || 120, notes: data.absNotes, }).then(function (res) { if (!res || !res.success || !res.url) return null; return API_BASE_URL + res.url; }).catch(function () { return null; }); } const instrumentId = (eng && eng.plugin_id) || (track && track.instrumentId); if (!instrumentId || !isVstTrackEngine(eng)) return Promise.resolve(null); return window.SonicAPI.midiRender({ instrument_id: instrumentId, notes: data.absNotes, bpm: parseFloat(bpm) || 120, preset_id: (eng && eng.preset_id) || undefined, preset_path: (eng && eng.preset_path) || undefined, preset_data: (eng && eng.preset_data) || undefined, }).then(function (res) { if (!res || !res.success || !res.url) return null; return API_BASE_URL + res.url; }).catch(function () { return null; }); } catch (e) { return Promise.resolve(null); } }; // Dedupe in-flight render + cache WAV: play l\u1eb7p/re-schedule trong l\u00fac render // \u0111ang ch\u1ea1y \u2192 ch\u1edd c\u00f9ng promise, kh\u00f4ng b\u1eafn duplicate request (tr\u00e1nh queue sau // RENDER_LOCK + render 2 l\u1ea7n c\u00f9ng n\u1ed9i dung). const ensureRenderNativeTrack = (track, bpm, baseOffsetSec, notesOverride) => { const key = _nativeTrackKey(track, bpm, baseOffsetSec, notesOverride); if (!key) return _renderNativeTrack(track, bpm, baseOffsetSec, notesOverride); if (_nativeTrackUrls[key]) return Promise.resolve(_nativeTrackUrls[key]); if (_nativeTrackPending[key]) return _nativeTrackPending[key]; const p = _renderNativeTrack(track, bpm, baseOffsetSec, notesOverride).then(function (url) { if (url) _nativeTrackUrls[key] = url; return url; }).then(function (url) { delete _nativeTrackPending[key]; return url; }); _nativeTrackPending[key] = p; return p; }; // Gi\u1edbi h\u1ea1n decodeAudioData song song (max 3) \u2014 nhi\u1ec1u WAV fetch v\u1ec1 c\u00f9ng l\u00fac \u2192 // main thread ngh\u1ebdn \u2192 UI \u0111\u01a1, Stop kh\u00f4ng ph\u1ea3n h\u1ed3i. let _nativeDecodeActive = 0; const _nativeDecodeQueue = []; const _pumpNativeDecodes = () => { while (_nativeDecodeActive < 3 && _nativeDecodeQueue.length) { const job = _nativeDecodeQueue.shift(); _nativeDecodeActive++; try { job.ctx.decodeAudioData(job.buf, function (ab) { _nativeDecodeActive--; job.cb(ab); _pumpNativeDecodes(); }, function () { _nativeDecodeActive--; job.cb(null); _pumpNativeDecodes(); }); } catch (e) { _nativeDecodeActive--; job.cb(null); _pumpNativeDecodes(); } } }; const _decodeNativeQueue = (ctx, buf, cb) => { _nativeDecodeQueue.push({ ctx: ctx, buf: buf, cb: cb }); _pumpNativeDecodes(); }; const scheduleNativeTrack = (track, offsetTime, context, destNode, bpm, opts) => { try { const eng = track && track.synth_engine; if (!isSfTrackEngine(eng) && !isVstTrackEngine(eng)) return; const baseOffsetSec = (opts && opts.baseOffsetSec) || 0; const notesOverride = (opts && opts.notesOverride) || null; const data = _buildNativeTrackNotes(track, bpm, baseOffsetSec, notesOverride); if (!data.absNotes.length) return; const key = _nativeTrackKey(track, bpm, baseOffsetSec, notesOverride); const epoch = _nativePlayEpoch; const scheduleUrl = function (url) { if (!url) return; fetch(url).then(function (r) { return r.arrayBuffer(); }).then(function (buf) { _decodeNativeQueue(context, buf, function (audioBuf) { try { if (!audioBuf) return; if (epoch !== _nativePlayEpoch) return; // stale play (stop/rebuild) if (opts && typeof opts.isActive === 'function' && !opts.isActive()) return; const ph = (opts && typeof opts.playheadNow === 'function') ? opts.playheadNow() : offsetTime; const src = context.createBufferSource(); src.buffer = audioBuf; src.connect(destNode); const trackEndAbs = data.trackStartAbs + audioBuf.duration; if (ph < data.trackStartAbs) { src.start(context.currentTime + (data.trackStartAbs - ph), 0); } else if (ph < trackEndAbs) { src.start(context.currentTime, Math.min(ph - data.trackStartAbs, Math.max(0, audioBuf.duration - 0.05))); } else { return; } if (opts && opts.limitSec && opts.limitSec < trackEndAbs) { src.stop(context.currentTime + Math.max(0.02, opts.limitSec - Math.max(ph, data.trackStartAbs))); } if (opts && opts.sources && opts.sources.push) opts.sources.push(src); } catch (e) { console.warn('[NativeTrack] schedule decode error:', e); } }); }).catch(function () {}); }; if (key && _nativeTrackUrls[key]) { scheduleUrl(_nativeTrackUrls[key]); return; } ensureRenderNativeTrack(track, bpm, baseOffsetSec, notesOverride).then(function (url) { if (!url) return; scheduleUrl(url); }); } catch (e) { console.warn('[NativeTrack] scheduleNativeTrack error:', e); } };""" src = src[:i0] + NEW + src[i1:] print("OK patch 1 (scheduleNativeSfItem -> scheduleNativeTrack)") else: print("SKIP patch 1 (da ap)") # ── Patch 2: URL cache per-item + prerenderNativeVstItems → prerenderNativeTracks ── if "prerenderNativeTracks" not in src: P2_START = "// URL cache cho midi-render: key = fingerprint (item+instrument+preset+notes)" P2_END = " }, 800);\n } catch (e) {}\n};" i0 = src.index(P2_START) i1 = src.index(P2_END, i0) + len(P2_END) P2 = """// \u2500\u2500 Native track pre-render (standalone) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 // Render C\u1ea2 TRACK 1 request khi project load \u2192 play l\u1ea7n \u0111\u1ea7u kh\u00f4ng ch\u1edd render. let _prerenderNativeTracksTimer = null; const prerenderNativeTracks = (tracks, bpm) => { try { if (!isStandaloneSf()) return; if (_prerenderNativeTracksTimer) { clearTimeout(_prerenderNativeTracksTimer); _prerenderNativeTracksTimer = null; } _prerenderNativeTracksTimer = setTimeout(function () { _prerenderNativeTracksTimer = null; const seen = {}; (tracks || []).forEach(function (track) { const eng = track && track.synth_engine; const native = isStandaloneSf() && (isVstTrackEngine(eng) || isSfTrackEngine(eng)) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()); if (!native) return; const key = _nativeTrackKey(track, bpm, 0, null); if (!key || seen[key] || _nativeTrackUrls[key]) return; seen[key] = true; ensureRenderNativeTrack(track, bpm, 0, null).then(function () {}); }); }, 800); } catch (e) {} };""" src = src[:i0] + P2 + src[i1:] print("OK patch 2 (prerenderNativeVstItems -> prerenderNativeTracks)") else: print("SKIP patch 2 (da ap)") # ── Patch 3: xóa scheduleNativeVstItem (không còn dùng) ── if "scheduleNativeVstItem" in src: P3_START = "// \u2500\u2500 Native VSTi item render (standalone, transport) \u2500" P3_END = " } catch (e) { console.warn('[NativeVST] scheduleNativeVstItem error:', e); }\n};" i0 = src.index(P3_START) i1 = src.index(P3_END) + len(P3_END) assert src[i1:i1+2] == "\n\n", repr(src[i1:i1+30]) src = src[:i0] + src[i1+2:] print("OK patch 3 (xoa scheduleNativeVstItem)") else: print("SKIP patch 3 (da ap)") # ── Patch 4: đổi 4 call sites sang scheduleNativeTrack ── C4 = [ # site 1: startTrackPlayback (""" if (nativeSf) { midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current })); } else if (nativeVst) { midiItems.forEach(item => scheduleNativeVstItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current })); } else {""", """ if (nativeSf || nativeVst) { scheduleNativeTrack(track, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }); } else {"""), # site 2: section playback (""" if (subNativeSf) { subMidiItems.forEach(item => scheduleNativeSfItem(subTrack, item, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current })); return; // native render \u2192 audio buffer (kh\u00f4ng qua WASM/Carla per-note) } if (subNativeVst) { subMidiItems.forEach(item => scheduleNativeVstItem(subTrack, item, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current })); return; // native render \u2192 audio buffer (kh\u00f4ng qua WASM/Carla per-note) }""", """ if (subNativeSf || subNativeVst) { scheduleNativeTrack(subTrack, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }); return; // native render \u2192 audio buffer (kh\u00f4ng qua WASM/Carla per-note) }"""), # site 3: startLocalTrackPlayback (""" if (nativeSf) { midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current })); return; // native render \u2192 audio buffer (kh\u00f4ng qua WASM/Carla per-note) } if (nativeVst) { midiItems.forEach(item => scheduleNativeVstItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current })); return; // native render \u2192 audio buffer (kh\u00f4ng qua WASM/Carla per-note) }""", """ if (nativeSf || nativeVst) { scheduleNativeTrack(track, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }); return; // native render \u2192 audio buffer (kh\u00f4ng qua WASM/Carla per-note) }"""), # site 4: piano roll (notesOverride) (""" if (isStandaloneSf() && !routeCarla && isSfTrackEngine(synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) { scheduleNativeSfItem(track, { startTime: 0, notes: midiNotes }, offsetSeconds, context, destNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }); return; // native render c\u1ea3 tab \u2192 audio buffer (kh\u00f4ng per-note WASM/Carla) } if (isStandaloneSf() && !routeCarla && isVstTrackEngine(synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) { scheduleNativeVstItem(track, { startTime: 0, notes: midiNotes }, offsetSeconds, context, destNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }); return; // native render c\u1ea3 tab \u2192 audio buffer (kh\u00f4ng per-note WASM/Carla) }""", """ if (isStandaloneSf() && !routeCarla && (isSfTrackEngine(synthEngine) || isVstTrackEngine(synthEngine)) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) { scheduleNativeTrack(track, offsetSeconds, context, destNode, bpm, { notesOverride: midiNotes, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }); return; // native render c\u1ea3 tab \u2192 audio buffer (kh\u00f4ng per-note WASM/Carla) }"""), ] n4 = 0 for old, new in C4: if old in src: src = src.replace(old, new, 1) n4 += 1 print("OK patch 4 (%d/4 call sites)" % n4) # ── Patch 5: stopAllPlayback tăng epoch ── if "_nativePlayEpoch++" not in src: anchor = " try {\n isPlayingRef.current = false;" assert src.count(anchor) == 1, "anchor stopAllPlayback khong duy nhat" src = src.replace(anchor, " try {\n isPlayingRef.current = false;\n _nativePlayEpoch++; // h\u1ee7y m\u1ecdi render/decode pending c\u1ee7a l\u1ea7n play c\u0169", 1) print("OK patch 5 (epoch guard trong stopAllPlayback)") else: print("SKIP patch 5 (da ap)") # ── Patch 6: đổi call sites prerender ── n6 = src.count("prerenderNativeVstItems(") if n6: src = src.replace("prerenderNativeVstItems(", "prerenderNativeTracks(") print("OK patch 6 (%d prerender call sites)" % n6) else: print("SKIP patch 6 (da ap)") save() print("DONE. changed=", src != orig, "len=", len(src))