Files
SonicForgeStudio/tools/patch_native_track.py
T
admin 7c22ae1f02 refactor(bridge): split fx bridge, stabilize VST3 host chain, drop old install engines
- remove diagnostic canaries from audio hot path (crackle fix) and
  stuck C5 note raw-MIDI test (Vst2Instrument::init noteOff)
- split native bridge into daw_vst_bridge (VSTi) + fx_vst_bridge (VST FX)
- VST3: processData prepare before setActive, zero input buses, accept
  both .vstpreset and bridge-state blob in RenderJob presets
- native_render/vst_engine: bridge-exe discovery + render_fx_chain + /fx-gui
- delete old install/ engine binaries (install via sidecar+resources now)
- add BUILD_LAYOUT.md, build_selfcheck, tools/ diagnostics
2026-08-20 09:42:42 +07:00

328 lines
18 KiB
Python

# -*- 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))