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
This commit is contained in:
2026-08-20 09:42:42 +07:00
parent 5df96aef1e
commit 7c22ae1f02
42 changed files with 2363 additions and 210 deletions
File diff suppressed because one or more lines are too long
+43
View File
@@ -0,0 +1,43 @@
// tools/check_gui_fix.js — self-check for the TCP GUI button fix (V9 regression).
// Run: node tools/check_gui_fix.js (exit 0 = OK)
// Verifies: guard exists in source, compiled bundle, and all 6 shipped copies
// are byte-identical to the compiled bundle; bundle is syntactically valid.
const { readFileSync } = require('fs');
const { execFileSync } = require('child_process');
const path = require('path');
const root = path.join(__dirname, '..');
const SRC = path.join(root, 'app/static/js/app.jsx');
const PRECOMPILED = path.join(root, 'app/static/js/app.precompiled.js');
const GUARD_SRC = 'if (!loadOk && !window.__bridgeLoadedChannels[bch]) {';
const GUARD_COMPILED = 'if(!loadOk&&!window.__bridgeLoadedChannels[bch]){';
const COPIES = [
'dist/daw_engine/_internal/app/static/js/app.precompiled.js',
'install/daw_engine/_internal/app/static/js/app.precompiled.js',
'src-tauri/resources/daw_engine/_internal/app/static/js/app.precompiled.js',
'src-tauri/target/debug/daw_engine/_internal/app/static/js/app.precompiled.js',
'src-tauri/target/release/daw_engine/_internal/app/static/js/app.precompiled.js'
];
function assert(cond, msg) {
if (!cond) { console.error('FAIL: ' + msg); process.exit(1); }
}
const src = readFileSync(SRC, 'utf8');
assert(src.includes(GUARD_SRC), 'guard missing in app.jsx');
const compiled = readFileSync(PRECOMPILED, 'utf8');
assert(compiled.includes(GUARD_COMPILED), 'guard missing in app.precompiled.js');
assert(compiled.includes('__bridgePluginPaths[instrumentId]=knownPath') || compiled.includes('window.__bridgePluginPaths[instrumentId]=knownPath'), 'path cache write missing in precompiled');
assert(src.includes('__bridgeLoadedState'), 'V10 loaded-state guard missing in app.jsx');
assert(compiled.includes('__bridgeLoadedState'), 'V10 loaded-state guard missing in precompiled');
// Bundle must parse (Babel output is plain script).
execFileSync(process.execPath, ['--check', PRECOMPILED], { stdio: 'pipe' });
for (const c of COPIES) {
const p = path.join(root, c);
assert(readFileSync(p, 'utf8') === compiled, 'copy out of sync: ' + c);
}
console.log('OK: GUI fix guard present in source + compiled + all ' + (COPIES.length + 1) + ' bundles; bundle parses.');
+22
View File
@@ -0,0 +1,22 @@
import io
p = 'app/api/v1/plugins.py'
s = io.open(p, encoding='utf-8', newline='').read()
a = '".vst2" if instrument_type.upper() == "VST2" else'
b = '(".dll", ".so") if instrument_type.upper() == "VST2" else'
assert s.count(a) == 1, s.count(a)
s = s.replace(a, b)
a2 = 'if fn.lower().endswith(ext) and \\'
b2 = 'if (fn.lower().endswith(ext) if isinstance(ext, tuple) else fn.lower().endswith(ext)) and \\'
assert s.count(a2) == 1, s.count(a2)
s = s.replace(a2, b2)
a3 = 'if os.path.isdir(sp) and sub.lower().endswith(ext) and \\'
b3 = 'if os.path.isdir(sp) and (sub.lower().endswith(ext) if isinstance(ext, tuple) else sub.lower().endswith(ext)) and \\'
assert s.count(a3) == 1, s.count(a3)
s = s.replace(a3, b3)
io.open(p, 'w', encoding='utf-8', newline='').write(s)
print('ok')
+30
View File
@@ -0,0 +1,30 @@
import io
p = 'app/core/vst_engine.py'
s = io.open(p, encoding='utf-8', newline='').read()
old = ''' def list_available(self) -> dict:
return {
"vst_instruments": [
{"id": k, "name": k, "type": "VST3", "has_native_support": HAS_PEDALBOARD}
for k in self._scan_plugins().keys()
],
"soundfonts": self._scan_soundfonts()
}'''
new = ''' def list_available(self) -> dict:
plugins = self._scan_plugins()
return {
"vst_instruments": [
{"id": k, "name": k,
"type": "VST3" if p.lower().endswith(".vst3") else "VST2",
"has_native_support": HAS_PEDALBOARD}
for k, p in plugins.items()
],
"soundfonts": self._scan_soundfonts()
}'''
assert old in s, 'not found'
s = s.replace(old, new)
io.open(p, 'w', encoding='utf-8', newline='').write(s)
print('ok')
+105
View File
@@ -0,0 +1,105 @@
import struct, sys
path = r"C:/Users/locpham/AppData/Local/Temp/sf_dumps/bridge_crash_1696.dmp"
data = open(path, "rb").read()
sig, ver, numstreams, sdir = struct.unpack_from("<IIIIIIQ", data, 0)[:4]
print("streams", numstreams, "sdir", hex(sdir))
streams = {}
for i in range(numstreams):
stype, datasize, rva = struct.unpack_from("<III", data, sdir + i * 12)
streams[stype] = (rva, datasize)
ThreadList = 3; ModuleList = 4; MemoryList = 5; Exception = 6; SystemInfo = 7
# Exception stream: ThreadId(4) align(4) MINIDUMP_EXCEPTION(code,flags,rec,addr,nparams,align,params[15]) ThreadContext
if Exception in streams:
rva, sz = streams[Exception]
tid, _ = struct.unpack_from("<II", data, rva)
code, flags, rec, exaddr, nparams, _ = struct.unpack_from("<IIQQII", data, rva + 8)
print("EXCEPTION tid", tid, "code", hex(code), "exaddr", hex(exaddr), "nparams", nparams)
for i in range(nparams):
p = struct.unpack_from("<Q", data, rva + 8 + 40 + i * 8)[0]
print(" param[%d]" % i, hex(p))
# Modules
mods = []
if ModuleList in streams:
rva, sz = streams[ModuleList]
n = struct.unpack_from("<I", data, rva)[0]
pos = rva + 4
for i in range(n):
base, imgsz, cksum, ts, name_rva = struct.unpack_from("<QIIII", data, pos)
nlen = struct.unpack_from("<I", data, name_rva)[0]
name = data[name_rva + 4:name_rva + 4 + nlen].decode("utf-16-le", "replace").rstrip("\x00")
mods.append((base, imgsz, name))
pos += 108 # MINIDUMP_MODULE
for base, szz, name in sorted(mods):
print("MOD %016x %8x %s" % (base, szz, name))
# Thread list: ThreadId, SuspendCount, PriorityClass, Priority, Teb, Stack(8), Context(8)
thr = {}
if ThreadList in streams:
rva, sz = streams[ThreadList]
n = struct.unpack_from("<I", data, rva)[0]
pos = rva + 4
for i in range(n):
tid2, susp, pric, pri, teb, st_start, st_sz, st_rva, ctx_sz, ctx_rva = struct.unpack_from("<IIIIQQIIII", data, pos)
thr[tid2] = (ctx_rva, ctx_sz, st_start, st_rva)
pos += 4 * 4 + 8 + 16 + 8
for tid2, (ctx_rva, ctx_sz, st_start, st_rva) in sorted(thr.items()):
# AMD64 CONTEXT: ContextFlags(4) ... Rax at offset 0x78? Use known layout:
# CONTEXT_AMD64: P1Home..P6Home(48) ContextFlags(4) MxCsr(4) SegCs(2)SegDs(2)SegEs(2)SegFs(2)SegGs(2)EFlags(4)Dr0..Dr3(32)Dr6(8)Dr7(8)Rax(8)Rcx(8)Rdx(8)Rbx(8)Rsp(8)Rbp(8)Rsi(8)Rdi(8)R8..R15(64)Rip(8)...
if not ctx_rva:
continue
cf = struct.unpack_from("<I", data, ctx_rva)[0]
# offsets (decimal) from CONTEXT_AMD64
o_rax = 0x78; o_rcx = 0x80; o_rdx = 0x88; o_rbx = 0x90; o_rsp = 0x98; o_rbp = 0xA0; o_rsi = 0xA8; o_rdi = 0xB0; o_r8 = 0xB8; o_r9 = 0xC0; o_rip = 0xF8
rax = struct.unpack_from("<Q", data, ctx_rva + o_rax)[0]
rcx = struct.unpack_from("<Q", data, ctx_rva + o_rcx)[0]
rdx = struct.unpack_from("<Q", data, ctx_rva + o_rdx)[0]
rbx = struct.unpack_from("<Q", data, ctx_rva + o_rbx)[0]
rsp = struct.unpack_from("<Q", data, ctx_rva + o_rsp)[0]
rbp = struct.unpack_from("<Q", data, ctx_rva + o_rbp)[0]
rsi = struct.unpack_from("<Q", data, ctx_rva + o_rsi)[0]
rdi = struct.unpack_from("<Q", data, ctx_rva + o_rdi)[0]
r8 = struct.unpack_from("<Q", data, ctx_rva + o_r8)[0]
r9 = struct.unpack_from("<Q", data, ctx_rva + o_r9)[0]
rip = struct.unpack_from("<Q", data, ctx_rva + o_rip)[0]
print("THREAD %d RIP %016x RSP %016x RBP %016x RAX %016x RCX %016x RDX %016x R8 %016x R9 %016x" % (tid2, rip, rsp, rbp, rax, rcx, rdx, r8, r9))
# Memory list for stack walk
mems = []
if MemoryList in streams:
rva, sz = streams[MemoryList]
n = struct.unpack_from("<I", data, rva)[0]
pos = rva + 4
for i in range(n):
start, msz, mrva = struct.unpack_from("<QII", data, pos)
mems.append((start, msz, mrva))
pos += 16
def mod_for(addr):
for base, szz, name in mods:
if base <= addr < base + szz:
return base, name
return None, None
if Exception in streams and ThreadList in streams:
ex_tid = struct.unpack_from("<I", data, streams[Exception][0])[0]
if ex_tid in thr:
ctx_rva, ctx_sz, st_start, st_rva = thr[ex_tid]
rip = struct.unpack_from("<Q", data, ctx_rva + 0xF8)[0]
rsp = struct.unpack_from("<Q", data, ctx_rva + 0x98)[0]
b, n = mod_for(rip)
print("\nCRASH THREAD %d RIP %016x -> %s+%x" % (ex_tid, rip, n.split("\\")[-1] if n else "?", (rip - b) if b else 0))
print("stack descriptor start=%016x rva=%x" % (st_start, st_rva))
if st_rva:
stack = data[st_rva:st_rva + ctx_sz * 2]
print("stack bytes available:", len(stack))
for j in range(0, min(len(stack), 16384) - 8, 8):
val = struct.unpack_from("<Q", stack, j)[0]
b2, n2 = mod_for(val)
if n2:
print(" +%04x %016x -> %s+%x" % (j, val, n2.split("\\")[-1], val - b2))
+345
View File
@@ -0,0 +1,345 @@
# Phase 3 UI patch: VST FX chain per-track (FX Rack) + masterbus (Mastering modal).
# CRLF-safe: normalize to LF, patch, restore CRLF.
import io, sys
PATH = 'app/static/js/app.jsx'
def main():
with io.open(PATH, encoding='utf-8', newline='') as f:
s = f.read()
crlf = '\r\n' in s
s = s.replace('\r\n', '\n')
orig = s
def sub(old, new):
nonlocal s
if old not in s:
print('NOT FOUND:', old[:90].replace('\n', '\\n'))
sys.exit(1)
if s.count(old) > 1:
print('NOT UNIQUE:', old[:90].replace('\n', '\\n'), 'count=', s.count(old))
sys.exit(1)
s = s.replace(old, new)
# 1. serializeTracksList — add vst_fx_chain + vst_fx_bypass
sub(
" fx_chain: (t.fxChain || []).map(m => typeof m === 'string' ? { type: m, active: true } : { type: m.type, active: m.active !== false }),\n"
" server_file_id: t.serverFileId || null,",
" fx_chain: (t.fxChain || []).map(m => typeof m === 'string' ? { type: m, active: true } : { type: m.type, active: m.active !== false }),\n"
" vst_fx_chain: (t.vstFxChain || []).map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass })),\n"
" vst_fx_bypass: !!t.vstFxBypass,\n"
" server_file_id: t.serverFileId || null,"
)
# 2. deserializeTracksList — restore vstFxChain + vstFxBypass
sub(
" fxChain: (t.fx_chain || []).map(m => typeof m === 'string' ? { type: m, active: true } : { type: m.type || m, active: m.active !== false }),\n"
" };",
" fxChain: (t.fx_chain || []).map(m => typeof m === 'string' ? { type: m, active: true } : { type: m.type || m, active: m.active !== false }),\n"
" vstFxChain: (t.vst_fx_chain || []).map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass })),\n"
" vstFxBypass: t.vst_fx_bypass !== undefined ? !!t.vst_fx_bypass : false,\n"
" };"
)
# 3. serializeProjectToSchema — master key (only when VST slots exist)
sub(
" tracks: mainTracks\n"
" },",
" tracks: mainTracks,\n"
" ...(masteringSettings && Array.isArray(masteringSettings.vstFxChain) && masteringSettings.vstFxChain.length > 0 ? {\n"
" master: {\n"
" fx_chain: masteringSettings.vstFxChain.map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass })),\n"
" volume_db: 0,\n"
" mute: false,\n"
" bypass: !!masteringSettings.vstBypass\n"
" }\n"
" } : {})\n"
" },"
)
# 4. deserializeProjectFromSchema — restore master VST chain
sub(
" return {\n"
" bpm: bpmVal,\n"
" tracks: restoredTracks,\n"
" sessionTabs: restoredSessionTabs,\n"
" subTabs: restoredSubTabs,\n"
" masteringSettings: _migrateMasteringSettings(schemaObj.mastering_settings),\n"
" zoom: schemaObj.metadata && schemaObj.metadata.zoom ? parseFloat(schemaObj.metadata.zoom) : 1.0\n"
" };",
" const _migratedMastering = _migrateMasteringSettings(schemaObj.mastering_settings) || {};\n"
" const _masterBus = (schemaObj.main_session && schemaObj.main_session.master) || {};\n"
" const _masterFx = Array.isArray(_masterBus.fx_chain) ? _masterBus.fx_chain : [];\n"
" return {\n"
" bpm: bpmVal,\n"
" tracks: restoredTracks,\n"
" sessionTabs: restoredSessionTabs,\n"
" subTabs: restoredSubTabs,\n"
" masteringSettings: {\n"
" ..._migratedMastering,\n"
" vstFxChain: _masterFx.map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass })),\n"
" vstBypass: !!_masterBus.bypass\n"
" },\n"
" zoom: schemaObj.metadata && schemaObj.metadata.zoom ? parseFloat(schemaObj.metadata.zoom) : 1.0\n"
" };"
)
# 5. masteringSettings initial state
sub(
" rebalActive: false, rebalMid: 0, rebalSide: 0,\n"
" });",
" rebalActive: false, rebalMid: 0, rebalSide: 0,\n"
" vstFxChain: [],\n"
" vstBypass: false,\n"
" });"
)
# 6. App scope: offline render preview (preview = âm export)
sub(
" window.__openFxRack = (trackId, trackName) => setFxRackTarget({ trackId, trackName });",
" window.__openFxRack = (trackId, trackName) => setFxRackTarget({ trackId, trackName });\n"
" // Render offline toàn project qua native_bridge (VST FX masterbus + track) → WAV.\n"
" // Dùng cho nút \"Nghe thử\" trong FX Rack / Mastering — preview = âm export.\n"
" window.__renderProjectPreview = async () => {\n"
" const schema = serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings);\n"
" try {\n"
" const res = await window.SonicAPI.renderProject(schema, 'preview_' + Date.now() + '.wav');\n"
" return res;\n"
" } catch (e) {\n"
" window.showToast && window.showToast('Render preview lỗi: ' + (e.message || e), 'error');\n"
" throw e;\n"
" }\n"
" };"
)
# 7. FXRackModal hooks — before early return
sub(
" }, [track && track.id]);\n"
"\n"
" if (!track) return null;",
" }, [track && track.id]);\n"
"\n"
" // VST FX (native_bridge --render-fx) — list VST3 effect plugins + preview\n"
" const [fxList, setFxList] = React.useState([]);\n"
" React.useEffect(() => {\n"
" if (window.SonicAPI && window.SonicAPI.listFx) {\n"
" window.SonicAPI.listFx().then(d => setFxList((d && d.plugins) || [])).catch(() => setFxList([]));\n"
" }\n"
" }, [track && track.id]);\n"
" const previewAudioRef = React.useRef(null);\n"
" const [vstBypass, setVstBypass] = React.useState(!!(track && track.vstFxBypass));\n"
" const [addVstSel, setAddVstSel] = React.useState('');\n"
"\n"
" if (!track) return null;"
)
# 8. FXRackModal helpers
sub(
" const chain = track.fxChain || [];\n"
" const setChain = (next) => {",
" const chain = track.fxChain || [];\n"
"\n"
" // ── VST FX chain (native_bridge --render-fx, áp dụng khi Export/Offline render) ──\n"
" const vstChain = (track.vstFxChain || []);\n"
" const setVstChain = (next) => { if (onUpdateTrack) onUpdateTrack(track.id, { vstFxChain: next, vstFxBypass: vstBypass }); };\n"
" const toggleVst = (idx) => setVstChain(vstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));\n"
" const removeVst = (idx) => setVstChain(vstChain.filter((_, i) => i !== idx));\n"
" const moveVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= vstChain.length) return; const next = [...vstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setVstChain(next); };\n"
" const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path); if (!fx) return; setVstChain([...vstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]); };\n"
" const changeVstPlugin = (idx, path) => { const fx = (fxList || []).find(f => f.path === path); setVstChain(vstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };\n"
" const uploadVstPreset = (idx) => {\n"
" const inp = document.createElement('input');\n"
" inp.type = 'file';\n"
" inp.accept = '.vstpreset,.fxp,.fxb,.dspreset';\n"
" inp.onchange = () => {\n"
" const f = inp.files && inp.files[0];\n"
" if (!f) return;\n"
" const reader = new FileReader();\n"
" reader.onload = () => {\n"
" const b64 = String(reader.result || '').split(',')[1] || '';\n"
" setVstChain(vstChain.map((s, i) => i === idx ? { ...s, preset_b64: b64 } : s));\n"
" window.showToast && window.showToast('Đã gắn preset .vstpreset vào slot ' + (idx + 1), 'success');\n"
" };\n"
" reader.readAsDataURL(f);\n"
" };\n"
" inp.click();\n"
" };\n"
" const previewFx = async () => {\n"
" try {\n"
" if (previewAudioRef.current) { previewAudioRef.current.pause(); previewAudioRef.current = null; }\n"
" if (!window.__renderProjectPreview) { window.showToast && window.showToast('Không có hàm render preview', 'warning'); return; }\n"
" window.showToast && window.showToast('Đang render offline (bridge)...', 'info');\n"
" const res = await window.__renderProjectPreview();\n"
" const au = new Audio((window.API_BASE_URL || '') + res.url);\n"
" previewAudioRef.current = au;\n"
" au.onended = () => { previewAudioRef.current = null; };\n"
" au.play().then(() => window.showToast && window.showToast('Preview offline — âm thật = âm export (VST FX)', 'success')).catch(() => {});\n"
" } catch (e) { /* toast ở __renderProjectPreview */ }\n"
" };\n"
"\n"
" const setChain = (next) => {"
)
# 9. FXRackModal JSX — VST FX section between CHAIN strip and WORKSPACE CONTROLS
vst_row = (
" const vstRow = (s, idx, toggleV, changeV, uploadV, moveV, removeV, fxl, chainLen) => (\n"
" <div key={idx} className=\"flex items-center gap-2 bg-slate-900/60 border border-slate-800 rounded-lg px-2 py-1.5 flex-wrap\">\n"
" <button onClick={() => toggleV(idx)} className={`w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0 ${!s.bypass ? 'bg-teal-500 text-slate-950' : 'bg-slate-700 text-slate-300'}`} title={s.bypass ? 'Bypass' : 'Active'}>\n"
" <i data-lucide=\"power\" className=\"w-2.5 h-2.5\"></i>\n"
" </button>\n"
" <span className=\"text-[10px] font-bold text-slate-300 font-mono w-6 shrink-0\">{idx + 1}</span>\n"
" <select value={s.path || ''} onChange={e => changeV(idx, e.target.value)} className=\"flex-1 min-w-[200px] bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1 text-[11px] outline-none focus:border-teal-500\">\n"
" <option value=\"\">— Chọn VST FX —</option>\n"
" {(fxl || []).map(f => <option key={f.path} value={f.path}>{f.name || f.id}</option>)}\n"
" </select>\n"
" <span className={`text-[9px] font-mono ${s.preset_b64 ? 'text-emerald-400' : 'text-slate-600'}`}>{s.preset_b64 ? '✓ preset' : 'no preset'}</span>\n"
" <button onClick={() => uploadV(idx)} className=\"px-2 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 text-[10px] font-semibold rounded transition\" title=\"Upload .vstpreset xuất từ Carla (Save preset)\">Preset</button>\n"
" <button onClick={() => { if (!s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } window.SonicAPI.openInCarla(s.name || s.path, s.path).then(r => { if (r && r.success) window.showToast && window.showToast('Đã mở Carla với ' + (s.name || s.path) + ' — chỉnh âm trong Carla rồi Save preset', 'success'); else window.showToast && window.showToast('Không mở được Carla', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở Carla: ' + (err.message || err), 'error')); }} className=\"px-2 py-1 bg-teal-800 hover:bg-teal-700 text-teal-100 text-[10px] font-semibold rounded transition\" title=\"Mở native GUI trong Carla\">Carla</button>\n"
" <button onClick={() => moveV(idx, -1)} disabled={idx === 0} className=\"w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30\" title=\"Lên\"><i data-lucide=\"chevron-up\" className=\"w-3 h-3\"></i></button>\n"
" <button onClick={() => moveV(idx, 1)} disabled={idx === chainLen - 1} className=\"w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30\" title=\"Xuống\"><i data-lucide=\"chevron-down\" className=\"w-3 h-3\"></i></button>\n"
" <button onClick={() => removeV(idx)} className=\"text-slate-600 hover:text-red-400 text-xs px-0.5\" title=\"Xóa slot\"><i data-lucide=\"x\" className=\"w-3 h-3\"></i></button>\n"
" </div>\n"
" );\n"
)
# Insert helper ABOVE the FXRackModal component so both modals can use it.
sub(
"const FXRackModal = ({ track, onUpdateTrack, onClose }) => {",
vst_row + "\n"
"const FXRackModal = ({ track, onUpdateTrack, onClose }) => {"
)
fxrack_vst_section = (
" {/* VST FX CHAIN (native_bridge --render-fx — áp dụng khi Export/Offline render) */}\n"
" <div className=\"bg-slate-950 border border-teal-900/60 rounded-xl px-3 py-2 flex flex-col gap-2 shrink-0 select-none\">\n"
" <div className=\"flex items-center justify-between flex-wrap gap-2\">\n"
" <div className=\"flex items-center gap-2 min-w-0\">\n"
" <span className=\"text-[10px] font-bold text-teal-400 uppercase tracking-widest font-mono shrink-0\">VST FX:</span>\n"
" <span className=\"text-[9px] text-slate-500 font-mono truncate\">Plugin VST3 (Ozone...) — realtime browser vẫn WebAudio builtin; VST áp dụng lúc Export/Offline render (preview = âm export)</span>\n"
" </div>\n"
" <div className=\"flex items-center gap-2 shrink-0\">\n"
" <button onClick={previewFx} className=\"px-2 py-1 bg-teal-800 hover:bg-teal-700 text-teal-100 text-[10px] font-semibold rounded transition flex items-center gap-1\">\n"
" <i data-lucide=\"play\" className=\"w-3 h-3\"></i> Nghe thử (render offline)\n"
" </button>\n"
" <label className=\"flex items-center gap-1 text-[10px] text-slate-400 font-mono cursor-pointer\">\n"
" <input type=\"checkbox\" checked={vstBypass} onChange={e => { const val = e.target.checked; setVstBypass(val); if (onUpdateTrack) onUpdateTrack(track.id, { vstFxBypass: val }); }} className=\"w-3 h-3\" /> Bypass\n"
" </label>\n"
" </div>\n"
" </div>\n"
" {vstChain.length === 0 && <div className=\"text-[10px] text-slate-500 font-mono\">Chưa có VST FX — chọn plugin bên dưới để thêm vào insert chain (WebAudio chain → VST FX → Fader).</div>}\n"
" {vstChain.map((s, idx) => vstRow(s, idx, toggleVst, changeVstPlugin, uploadVstPreset, moveVst, removeVst, fxList, vstChain.length))}\n"
" <select value={addVstSel} onChange={e => { setAddVstSel(e.target.value); if (e.target.value) { addVst(e.target.value); setAddVstSel(''); } }} className=\"w-full bg-slate-950 border border-dashed border-slate-700 hover:border-teal-600 text-teal-300 rounded px-2 py-1.5 text-[11px] outline-none\" title=\"Thêm VST FX\">\n"
" <option value=\"\">+ Thêm VST FX...</option>\n"
" {(fxList || []).map(f => <option key={f.path} value={f.path}>{f.name || f.id}</option>)}\n"
" </select>\n"
" </div>\n"
"\n"
" {/* WORKSPACE CONTROLS */}"
)
sub(
" {/* WORKSPACE CONTROLS */}",
fxrack_vst_section
)
# 10. MasteringModal hooks
sub(
" const knobsInitializedRef = React.useRef(false);",
" const knobsInitializedRef = React.useRef(false);\n"
"\n"
" // VST FX masterbus (native_bridge --render-fx) — list VST3 effect plugins + preview\n"
" const [masterFxList, setMasterFxList] = React.useState([]);\n"
" React.useEffect(() => {\n"
" if (window.SonicAPI && window.SonicAPI.listFx) {\n"
" window.SonicAPI.listFx().then(d => setMasterFxList((d && d.plugins) || [])).catch(() => setMasterFxList([]));\n"
" }\n"
" }, []);\n"
" const previewAudioRef = React.useRef(null);\n"
" const [addMasterVstSel, setAddMasterVstSel] = React.useState('');"
)
# 11. MasteringModal helpers
sub(
" const audioRef = React.useRef({ source: null });",
" // ── VST FX masterbus (native_bridge --render-fx, áp dụng khi Export/Offline render) ──\n"
" const masterVstChain = (ozState.vstFxChain || []);\n"
" const setMasterVstChain = (next) => setOzState(prev => ({ ...prev, vstFxChain: next }));\n"
" const toggleMasterVst = (idx) => setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));\n"
" const removeMasterVst = (idx) => setMasterVstChain(masterVstChain.filter((_, i) => i !== idx));\n"
" const moveMasterVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= masterVstChain.length) return; const next = [...masterVstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setMasterVstChain(next); };\n"
" const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.path === path); if (!fx) return; setMasterVstChain([...masterVstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]); };\n"
" const changeMasterVstPlugin = (idx, path) => { const fx = (masterFxList || []).find(f => f.path === path); setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };\n"
" const uploadMasterVstPreset = (idx) => {\n"
" const inp = document.createElement('input');\n"
" inp.type = 'file';\n"
" inp.accept = '.vstpreset,.fxp,.fxb,.dspreset';\n"
" inp.onchange = () => {\n"
" const f = inp.files && inp.files[0];\n"
" if (!f) return;\n"
" const reader = new FileReader();\n"
" reader.onload = () => {\n"
" const b64 = String(reader.result || '').split(',')[1] || '';\n"
" setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, preset_b64: b64 } : s));\n"
" window.showToast && window.showToast('Đã gắn preset .vstpreset vào slot ' + (idx + 1), 'success');\n"
" };\n"
" reader.readAsDataURL(f);\n"
" };\n"
" inp.click();\n"
" };\n"
" const previewMasterFx = async () => {\n"
" try {\n"
" if (previewAudioRef.current) { previewAudioRef.current.pause(); previewAudioRef.current = null; }\n"
" if (!window.__renderProjectPreview) { window.showToast && window.showToast('Không có hàm render preview', 'warning'); return; }\n"
" window.showToast && window.showToast('Đang render offline (bridge)...', 'info');\n"
" const res = await window.__renderProjectPreview();\n"
" const au = new Audio((window.API_BASE_URL || '') + res.url);\n"
" previewAudioRef.current = au;\n"
" au.onended = () => { previewAudioRef.current = null; };\n"
" au.play().then(() => window.showToast && window.showToast('Preview offline — âm thật = âm export (VST FX)', 'success')).catch(() => {});\n"
" } catch (e) { /* toast ở __renderProjectPreview */ }\n"
" };\n"
"\n"
" const audioRef = React.useRef({ source: null });"
)
# 12. MasteringModal JSX — VST section after CHAIN strip, before MAIN WORKSPACE
master_vst_section = (
" {/* VST FX MASTERBUS (native_bridge --render-fx — áp dụng khi Export/Offline render) */}\n"
" <div className=\"bg-slate-950 border-b border-teal-900/60 px-4 py-2 flex flex-col gap-2 shrink-0\">\n"
" <div className=\"flex items-center justify-between flex-wrap gap-2\">\n"
" <div className=\"flex items-center gap-2 min-w-0\">\n"
" <span className=\"text-[10px] font-bold text-teal-400 uppercase tracking-widest oz-font-mono shrink-0\">VST FX MASTERBUS:</span>\n"
" <span className=\"text-[9px] text-slate-500 oz-font-mono truncate\">Plugin VST3 (Ozone...) — realtime browser vẫn WebAudio builtin; VST áp dụng lúc Export/Offline render (preview = âm export)</span>\n"
" </div>\n"
" <div className=\"flex items-center gap-2 shrink-0\">\n"
" <button onClick={previewMasterFx} className=\"px-2 py-1 bg-teal-800 hover:bg-teal-700 text-teal-100 text-[10px] font-semibold rounded transition flex items-center gap-1\">\n"
" <i data-lucide=\"play\" className=\"w-3 h-3\"></i> Nghe thử (render offline)\n"
" </button>\n"
" <label className=\"flex items-center gap-1 text-[10px] text-slate-400 oz-font-mono cursor-pointer\">\n"
" <input type=\"checkbox\" checked={!!ozState.vstBypass} onChange={e => setOzState(prev => ({ ...prev, vstBypass: e.target.checked }))} className=\"w-3 h-3\" /> Bypass\n"
" </label>\n"
" </div>\n"
" </div>\n"
" {masterVstChain.length === 0 && <div className=\"text-[10px] text-slate-500 oz-font-mono\">Chưa có VST FX masterbus — chọn plugin bên dưới (Ozone...) để chạy chain sau khi mix WebAudio.</div>}\n"
" {masterVstChain.map((s, idx) => vstRow(s, idx, toggleMasterVst, changeMasterVstPlugin, uploadMasterVstPreset, moveMasterVst, removeMasterVst, masterFxList, masterVstChain.length))}\n"
" <select value={addMasterVstSel} onChange={e => { setAddMasterVstSel(e.target.value); if (e.target.value) { addMasterVst(e.target.value); setAddMasterVstSel(''); } }} className=\"w-full bg-slate-950 border border-dashed border-slate-700 hover:border-teal-600 text-teal-300 rounded px-2 py-1.5 text-[11px] outline-none\" title=\"Thêm VST FX\">\n"
" <option value=\"\">+ Thêm VST FX...</option>\n"
" {(masterFxList || []).map(f => <option key={f.path} value={f.path}>{f.name || f.id}</option>)}\n"
" </select>\n"
" </div>\n"
"\n"
" {/* MAIN WORKSPACE */}"
)
sub(
" {/* MAIN WORKSPACE */}",
master_vst_section
)
if s == orig:
print('NO CHANGE — nothing patched')
sys.exit(1)
with io.open(PATH, 'w', encoding='utf-8', newline='') as f:
f.write(s if not crlf else s.replace('\n', '\r\n'))
print('OK patched app.jsx')
if __name__ == '__main__':
main()
+80
View File
@@ -0,0 +1,80 @@
import io, sys
p = r"C:/Users/locpham/SonicForgeStudio/native_bridge/src/main.cpp"
data = open(p, "rb").read()
def rep(old, new, count=1):
global data
old = old.encode()
new = new.encode()
assert data.count(old) >= count, "pattern not found: %r" % old
data = data.replace(old, new, count)
# 1. includes: cstdio + dbghelp.h into the _WIN32 block
rep("#include <cstdlib>\n#include <thread>\n",
"#include <cstdlib>\n#include <cstdio>\n#include <dbghelp.h>\n#include <thread>\n")
# 2. handler + registration at main entry
handler = r"""
// --- crash minidump handler (temp diagnostics) -------------------------------
// WER LocalDumps produces NO dump for 0xC0000374 (heap corruption) — write our
// own minidump via dbghelp.dll when an unhandled exception reaches this filter.
#ifdef _WIN32
static LONG WINAPI CrashDumpHandler(EXCEPTION_POINTERS* ep) {
static LONG once = 0;
if (InterlockedCompareExchange(&once, 1, 0)) return EXCEPTION_CONTINUE_SEARCH;
PEXCEPTION_RECORD er = ep ? ep->ExceptionRecord : nullptr;
std::cerr << "[CRASH] exception code=0x" << std::hex
<< (er ? er->ExceptionCode : 0) << " addr=0x"
<< (er ? (void*)er->ExceptionAddress : nullptr) << std::dec << std::endl;
if (er) {
HMODULE hMod = nullptr;
char modPath[MAX_PATH] = {0};
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCSTR)er->ExceptionAddress, &hMod) && hMod) {
GetModuleFileNameA(hMod, modPath, MAX_PATH);
std::cerr << "[CRASH] faulting module: " << modPath << std::endl;
}
}
const char* dumpDir = "C:/Users/locpham/AppData/Local/Temp/sf_dumps";
CreateDirectoryA(dumpDir, nullptr);
char dumpPath[MAX_PATH];
snprintf(dumpPath, sizeof(dumpPath), "%s/bridge_crash_%lu.dmp", dumpDir, GetCurrentProcessId());
HMODULE hDbg = LoadLibraryA("dbghelp.dll");
if (hDbg) {
typedef BOOL(WINAPI* MiniDumpWriteDumpFn)(HANDLE, DWORD, HANDLE, MINIDUMP_TYPE,
PMINIDUMP_EXCEPTION_INFORMATION, PMINIDUMP_USER_STREAM_INFORMATION,
PMINIDUMP_CALLBACK_INFORMATION);
auto pMiniDumpWriteDump = (MiniDumpWriteDumpFn)GetProcAddress(hDbg, "MiniDumpWriteDump");
if (pMiniDumpWriteDump) {
HANDLE hFile = CreateFileA(dumpPath, GENERIC_WRITE, 0, nullptr,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION mei = {};
mei.ThreadId = GetCurrentThreadId();
mei.ExceptionPointers = ep;
mei.ClientPointers = TRUE;
BOOL ok = pMiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
hFile, MiniDumpNormal, &mei, nullptr, nullptr);
CloseHandle(hFile);
std::cerr << "[CRASH] minidump " << (ok ? "written: " : "FAILED: ") << dumpPath << std::endl;
} else {
std::cerr << "[CRASH] cannot open dump file" << std::endl;
}
} else {
std::cerr << "[CRASH] MiniDumpWriteDump not found in dbghelp.dll" << std::endl;
}
} else {
std::cerr << "[CRASH] cannot load dbghelp.dll" << std::endl;
}
return EXCEPTION_CONTINUE_SEARCH; // let WER proceed, process dies normally
}
#endif
"""
rep("int main(int argc, char* argv[]) {\n",
handler + "\nint main(int argc, char* argv[]) {\n SetUnhandledExceptionFilter(CrashDumpHandler);\n")
open(p, "wb").write(data)
print("patched OK, size=", len(data))
+327
View File
@@ -0,0 +1,327 @@
# -*- 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))
+8
View File
@@ -0,0 +1,8 @@
import io
p = 'tools/test_vst2_host.py'
s = io.open(p, encoding='utf-8', newline='').read()
old = "PLUGIN = r'C:\\Program Files\\Steinberg\\VSTPlugins\\DUNE 3.dll'"
assert old in s, 'not found'
s = s.replace(old, "PLUGIN = r'C:\\Program Files\\Steinberg\\VSTPlugins\\WaveObserver.dll'")
io.open(p, 'w', encoding='utf-8', newline='').write(s)
print('patched')
+54
View File
@@ -0,0 +1,54 @@
import sys
p = r"C:/Users/locpham/SonicForgeStudio/native_bridge/src/main.cpp"
data = open(p, "rb").read()
def rep(old, new, count=1):
global data
old = old.encode(); new = new.encode()
assert data.count(old) >= count, "pattern not found: %r" % old
data = data.replace(old, new, count)
# unwind stack from exception context BEFORE the dump writer clobbers it
stackdump = r"""
// Unwind the LIVE stack from the exception context (MiniDumpWriteDump captures
// the context at dump time, not at raise time — by then the handler's own
// frames replace the real ones). Print return-address candidates that land in
// loaded modules, lowest stack address first.
#ifdef _WIN32
static void DumpStackTraceFromContext(const CONTEXT* ctx) {
if (!ctx) return;
std::cerr << "[VCRASH] stack trace (raw scan):" << std::endl;
const unsigned char* sp = (const unsigned char*)ctx->Rsp;
for (int i = 0; i < 512; i++) {
uintptr_t val = 0;
memcpy(&val, sp + i * 8, sizeof(val)); // stack is readable in a handler
HMODULE h = nullptr;
char m[MAX_PATH] = {0};
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCSTR)val, &h) && h && GetModuleFileNameA(h, m, MAX_PATH)) {
const char* base = strrchr(m, '\\');
base = base ? base + 1 : m;
std::cerr << " +" << i * 8 << " " << (void*)val << " " << base
<< "+0x" << std::hex << (val - (uintptr_t)h) << std::dec << std::endl;
}
}
}
#endif
"""
rep("static LONG WINAPI VectoredCrashHandler(PEXCEPTION_POINTERS ep) {",
stackdump + "\nstatic LONG WINAPI VectoredCrashHandler(PEXCEPTION_POINTERS ep) {")
# call it right at handler entry
rep(""" std::cerr << "[VCRASH] first-chance code=0x" << std::hex << code
<< " addr=0x" << (er ? (void*)er->ExceptionAddress : nullptr)
<< std::dec << " tid=" << GetCurrentThreadId() << std::endl;""",
""" std::cerr << "[VCRASH] first-chance code=0x" << std::hex << code
<< " addr=0x" << (er ? (void*)er->ExceptionAddress : nullptr)
<< std::dec << " tid=" << GetCurrentThreadId() << std::endl;
DumpStackTraceFromContext(ep ? ep->ContextRecord : nullptr);""")
open(p, "wb").write(data)
print("patched OK, size=", len(data))
+39
View File
@@ -0,0 +1,39 @@
import sys
p = r"C:/Users/locpham/SonicForgeStudio/native_bridge/src/main.cpp"
data = open(p, "rb").read()
def rep(old, new, count=1):
global data
old = old.encode(); new = new.encode()
assert data.count(old) >= count, "pattern not found: %r" % old
data = data.replace(old, new, count)
# 1. vectored handler ghi dump cho first-chance; cai gan CrashDumpHandler
vectored = r"""
// Vectored handler: catches first-chance exceptions INCLUDING heap-corruption
// raises that never reach SetUnhandledExceptionFilter (0xC0000374 observed to
// bypass it on this machine). Writes the same minidump then continues.
#ifdef _WIN32
static LONG WINAPI VectoredCrashHandler(PEXCEPTION_POINTERS ep) {
PEXCEPTION_RECORD er = ep ? ep->ExceptionRecord : nullptr;
DWORD code = er ? er->ExceptionCode : 0;
if (code != 0xC0000374 && code != 0xC0000005 && code != 0xC0000409) {
return EXCEPTION_CONTINUE_SEARCH; // ignore benign first-chance noise
}
std::cerr << "[VCRASH] first-chance code=0x" << std::hex << code
<< " addr=0x" << (er ? (void*)er->ExceptionAddress : nullptr)
<< std::dec << " tid=" << GetCurrentThreadId() << std::endl;
// route into the same dump writer
return CrashDumpHandler(ep) == EXCEPTION_CONTINUE_SEARCH
? EXCEPTION_CONTINUE_SEARCH
: EXCEPTION_CONTINUE_SEARCH;
}
#endif
"""
rep("int main(int argc, char* argv[]) {\n SetUnhandledExceptionFilter(CrashDumpHandler);\n",
vectored + "\nint main(int argc, char* argv[]) {\n SetUnhandledExceptionFilter(CrashDumpHandler);\n AddVectoredExceptionHandler(1, VectoredCrashHandler);\n")
open(p, "wb").write(data)
print("patched OK, size=", len(data))
+54
View File
@@ -0,0 +1,54 @@
import struct
path = r"C:/Users/locpham/AppData/Local/Temp/sf_dumps/bridge_crash_1696.dmp"
data = open(path, "rb").read()
sig, ver, numstreams, sdir = struct.unpack_from("<IIIIIIQ", data, 0)[:4]
streams = {}
for i in range(numstreams):
stype, datasize, rva = struct.unpack_from("<III", data, sdir + i * 12)
streams[stype] = (rva, datasize)
mods = []
rva, sz = streams[4]
n = struct.unpack_from("<I", data, rva)[0]
pos = rva + 4
for i in range(n):
base, imgsz, cksum, ts, name_rva = struct.unpack_from("<QIIII", data, pos)
nlen = struct.unpack_from("<I", data, name_rva)[0]
name = data[name_rva + 4:name_rva + 4 + nlen].decode("utf-16-le", "replace").rstrip("\x00")
mods.append((base, imgsz, name))
pos += 108
def mod_for(addr):
for base, szz, name in mods:
if base <= addr < base + szz:
return base, name
return None, None
# dump all memory regions, scan for qwords inside DUNE 3.dll or bridge exe
targets = []
for base, szz, name in mods:
if "DUNE" in name or "daw_vst_bridge" in name:
targets.append((base, szz, name))
print("targets:", [(hex(b), s, n.split("\\")[-1]) for b, s, n in targets])
rva, sz = streams[5]
n = struct.unpack_from("<I", data, rva)[0]
pos = rva + 4
hits = {}
for i in range(n):
start, msz, mrva = struct.unpack_from("<QII", data, pos)
pos += 16
chunk = data[mrva:mrva + msz]
for j in range(0, len(chunk) - 8, 8):
val = struct.unpack_from("<Q", chunk, j)[0]
for tb, tsz, tn in targets:
if tb <= val < tb + tsz:
hits.setdefault((val, tn, val - tb), []).append((start + j, msz))
break
print("\nunique qwords pointing into DUNE/bridge (addr, module, offset, count):")
for (val, tn, off), locs in sorted(hits.items()):
print(" %016x %s+%x (seen %d times, e.g. mem@%016x size %x)" % (val, tn.split("\\")[-1], off, len(locs), locs[0][0], locs[0][1]))
+78
View File
@@ -0,0 +1,78 @@
# Inject note-on (live=1) and measure master buffer energy while held.
import ctypes, sys, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
APP_PID = sys.argv[1]
SHM_NAME = f'SonicForge_DAW_IPC_{APP_PID}'
OFF_MQ = 2064
OFF_MQ_COUNT = 2832
OFF_CQ = 2836
OFF_CQ_COUNT = 11156
OFF_MASTER_L = 8
OFF_MASTER_R = 1032
MIDI_ENTRY = 12
CTRL_ENTRY = 1040
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
k32.OpenFileMappingA.restype = ctypes.c_void_p
k32.OpenFileMappingA.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.c_char_p]
k32.MapViewOfFile.restype = ctypes.c_void_p
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
k32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
k32.CloseHandle.argtypes = [ctypes.c_void_p]
h = k32.OpenFileMappingA(0x0002, 0, SHM_NAME.encode())
if not h: print('FATAL open', ctypes.get_last_error()); sys.exit(1)
ptr = k32.MapViewOfFile(h, 0x0002, 0, 0, 0)
if not ptr: print('FATAL map', ctypes.get_last_error()); sys.exit(1)
def send_ctrl(type_, arg0):
idx = ctypes.c_uint32.from_address(ptr + OFF_CQ_COUNT).value
base = ptr + OFF_CQ + idx * CTRL_ENTRY
ctypes.c_uint32.from_address(base + 0).value = type_
ctypes.c_uint32.from_address(base + 4).value = arg0
ctypes.c_uint32.from_address(ptr + OFF_CQ_COUNT).value = idx + 1
def send_midi(cmd, ch, pitch, vel, live, data2=0, data3=0):
idx = ctypes.c_uint32.from_address(ptr + OFF_MQ_COUNT).value
base = ptr + OFF_MQ + idx * MIDI_ENTRY
ctypes.c_uint8.from_address(base + 0).value = cmd
ctypes.c_uint8.from_address(base + 1).value = ch
ctypes.c_uint8.from_address(base + 2).value = pitch
ctypes.c_uint8.from_address(base + 3).value = vel
ctypes.c_uint8.from_address(base + 4).value = data2
ctypes.c_uint8.from_address(base + 5).value = data3
ctypes.c_uint8.from_address(base + 6).value = 1 if live else 0
ctypes.c_uint8.from_address(base + 7).value = 0
ctypes.c_uint32.from_address(base + 8).value = 0
ctypes.c_uint32.from_address(ptr + OFF_MQ_COUNT).value = idx + 1
def energy(off):
arr = (ctypes.c_float * 256).from_address(ptr + off)
return sum(abs(v) for v in arr)
def sample(label, n=8):
tot = 0.0
for _ in range(n):
tot += energy(OFF_MASTER_L) + energy(OFF_MASTER_R)
time.sleep(0.05)
print(f'{label}: avg energy = {tot/(2*n):.8f}')
print('baseline (no note)')
sample('before', 5)
print('--- NOTE_ON ch=0 p=60 vel=100 live=1 ---')
send_midi(0x9, 0, 60, 100, live=1)
time.sleep(0.8)
sample('held', 8)
print('--- NOTE_ON ch=0 p=72 vel=120 live=1 ---')
send_midi(0x9, 0, 72, 120, live=1)
time.sleep(0.8)
sample('held2', 8)
print('--- NOTE_OFF ---')
send_midi(0x9, 0, 60, 0, live=1)
send_midi(0x9, 0, 72, 0, live=1)
time.sleep(0.5)
sample('after off', 5)
k32.UnmapViewOfFile(ptr); k32.CloseHandle(h)
print('DONE')
+54
View File
@@ -0,0 +1,54 @@
# Read SHM state: heartbeat/writeIndex advance + master buffer energy.
import ctypes, sys, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
APP_PID = sys.argv[1]
SHM_NAME = f'SonicForge_DAW_IPC_{APP_PID}'
# offsets from header: clientReadIndex@0, bridgeWriteIndex@4, masterLeft@8 (256*4),
# masterRight@1032, blockTimestamp@2056, midiQueue@2064, midiQueueCount@2832,
# controlQueue@2836, controlQueueCount@11156, attachInFlight@11160, heartbeat@11164
OFF_CLIENT_READ = 0
OFF_BRIDGE_WRITE = 4
OFF_MASTER_L = 8
OFF_MASTER_R = 1032
OFF_HEARTBEAT = 11164
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
k32.OpenFileMappingA.restype = ctypes.c_void_p
k32.OpenFileMappingA.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.c_char_p]
k32.MapViewOfFile.restype = ctypes.c_void_p
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
k32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
k32.CloseHandle.argtypes = [ctypes.c_void_p]
h = k32.OpenFileMappingA(0x0002, 0, SHM_NAME.encode())
if not h:
print('FATAL: open SHM failed', ctypes.get_last_error())
sys.exit(1)
ptr = k32.MapViewOfFile(h, 0x0002, 0, 0, 0)
if not ptr:
print('FATAL: map failed', ctypes.get_last_error())
sys.exit(1)
def r32(off): return ctypes.c_uint32.from_address(ptr + off).value
def energy(off):
arr = (ctypes.c_float * 256).from_address(ptr + off)
return sum(abs(v) for v in arr)
samples = []
for i in range(20):
hb = r32(OFF_HEARTBEAT)
wi = r32(OFF_BRIDGE_WRITE)
eL = energy(OFF_MASTER_L)
eR = energy(OFF_MASTER_R)
samples.append((hb, wi, eL, eR))
time.sleep(0.15)
print('sample hb wi eL eR')
for s in samples: print(s)
print('hb advanced:', len(set(s[0] for s in samples)) > 1)
print('wi advanced:', len(set(s[1] for s in samples)) > 1)
print('max eL:', max(s[2] for s in samples), 'max eR:', max(s[3] for s in samples))
k32.UnmapViewOfFile(ptr)
k32.CloseHandle(h)
+96
View File
@@ -0,0 +1,96 @@
# SHM injection test for live-flag fix: verify transportStopped guard drops
# non-live note-ons but passes live ones (reserved[0]=1).
# Layout from SharedMemoryIPC.h (verified): base=2064 bytes, midiQueue 64x12 @2064,
# midiQueueCount @2832, controlQueue 8x1040 @2836, controlQueueCount @11156.
import ctypes, sys, time, os
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
APP_PID = sys.argv[1] if len(sys.argv) > 1 else '9712'
SHM_NAME = f'SonicForge_DAW_IPC_{APP_PID}'
OFF_MQ = 2064
OFF_MQ_COUNT = 2832
OFF_CQ = 2836
OFF_CQ_COUNT = 11156
MIDI_ENTRY = 12
CTRL_ENTRY = 1040
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
k32.OpenFileMappingA.restype = ctypes.c_void_p
k32.OpenFileMappingA.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.c_char_p]
k32.MapViewOfFile.restype = ctypes.c_void_p
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
k32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
k32.CloseHandle.argtypes = [ctypes.c_void_p]
def get_shm():
h = k32.OpenFileMappingA(0x0002, 0, SHM_NAME.encode()) # FILE_MAP_WRITE first
if not h:
h = k32.OpenFileMappingA(0x0004, 0, SHM_NAME.encode()) # fallback FILE_MAP_READ
if not h:
err = ctypes.get_last_error()
print(f'FATAL: cannot open SHM {SHM_NAME} (err={err})')
sys.exit(1)
ptr = k32.MapViewOfFile(h, 0x0002, 0, 0, 0) # FILE_MAP_WRITE
if not ptr:
print(f'FATAL: MapViewOfFile err={ctypes.get_last_error()}')
sys.exit(1)
return h, ptr
def w32(ptr, off, val):
ctypes.c_uint32.from_address(ptr + off).value = val
def send_ctrl(ptr, type_, arg0, arg1=0, channel=0):
idx = ctypes.c_uint32.from_address(ptr + OFF_CQ_COUNT).value
if idx >= 8:
print('FATAL: control queue full')
sys.exit(1)
base = ptr + OFF_CQ + idx * CTRL_ENTRY
ctypes.c_uint32.from_address(base + 0).value = type_
ctypes.c_uint32.from_address(base + 4).value = arg0
ctypes.c_uint32.from_address(base + 8).value = arg1
ctypes.c_uint32.from_address(base + 12).value = channel
ctypes.c_uint32.from_address(ptr + OFF_CQ_COUNT).value = idx + 1
print(f'sent ctrl type={type_} arg0={arg0}')
def send_midi(ptr, cmd, channel, pitch, velocity, live, data2=0, data3=0):
idx = ctypes.c_uint32.from_address(ptr + OFF_MQ_COUNT).value
if idx >= 64:
print('FATAL: midi queue full')
sys.exit(1)
base = ptr + OFF_MQ + idx * MIDI_ENTRY
ctypes.c_uint8.from_address(base + 0).value = cmd
ctypes.c_uint8.from_address(base + 1).value = channel
ctypes.c_uint8.from_address(base + 2).value = pitch
ctypes.c_uint8.from_address(base + 3).value = velocity
ctypes.c_uint8.from_address(base + 4).value = data2
ctypes.c_uint8.from_address(base + 5).value = data3
ctypes.c_uint8.from_address(base + 6).value = 1 if live else 0 # reserved[0] = live flag
ctypes.c_uint8.from_address(base + 7).value = 0
ctypes.c_uint32.from_address(base + 8).value = 0 # sampleOffset
ctypes.c_uint32.from_address(ptr + OFF_MQ_COUNT).value = idx + 1
print(f'sent midi cmd=0x{cmd:x} ch={channel} pitch={pitch} vel={velocity} live={live}')
h, ptr = get_shm()
print(f'SHM {SHM_NAME} mapped at {ptr:#x}')
# 1) transport STOP -> guard arms
send_ctrl(ptr, 3, 0) # TRANSPORT STOP
time.sleep(1.0)
# 2) non-live NOTE_ON -> must be dropped (no [midi] ON log)
send_midi(ptr, 0x9, 0, 60, 100, live=0)
time.sleep(1.0)
# 3) live NOTE_ON -> must pass
send_midi(ptr, 0x9, 0, 60, 100, live=1)
time.sleep(1.0)
# 4) non-live NOTE_ON again -> dropped (sanity)
send_midi(ptr, 0x9, 0, 62, 90, live=0)
time.sleep(1.0)
# 5) restore PLAY so app continues working
send_ctrl(ptr, 3, 1)
time.sleep(0.5)
k32.UnmapViewOfFile(ptr)
k32.CloseHandle(h)
print('DONE')
+59
View File
@@ -0,0 +1,59 @@
// tools/test_midi_polyphony.js — runnable check cho Phase A (dedup/legato/polyphony).
// Đọc app.jsx, extract 2 hàm thuần, eval, assert spec §II.1 + §II.5.
const fs = require('fs');
const path = require('path');
const src = fs.readFileSync(path.join(__dirname, '..', 'app', 'static', 'js', 'app.jsx'), 'utf8');
const start = src.indexOf('const _cleanNativeNotes = (notes) => {');
const end = src.indexOf('const _nativeTrackKey', start);
if (start < 0 || end < 0) { console.error('FAIL: khong tim thay 2 ham'); process.exit(1); }
const code = src.slice(start, end) +
';globalThis._cleanNativeNotes=_cleanNativeNotes;' +
'globalThis._scaleNativePolyphony=_scaleNativePolyphony;';
// eslint-disable-next-line no-eval
(0, eval)(code); // indirect eval: const khai báo ở global scope
let fails = 0;
const assert = (cond, msg) => { if (!cond) { fails++; console.error('FAIL: ' + msg); } else { console.log('ok: ' + msg); } };
const near = (a, b, eps) => Math.abs(a - b) < (eps || 1e-9);
// 1. 8-note chord cùng lúc → tổng peak ≈ 0.3*8/sqrt(8) ≤ 1.0
const chord = [];
for (let i = 0; i < 8; i++) chord.push({ pitch: 60 + i, start_beat: 0, duration_beats: 2, velocity: 0.3 });
const sc = _scaleNativePolyphony(_cleanNativeNotes(chord));
const sum = sc.reduce((a, n) => a + n.velocity, 0);
assert(sc.length === 8, 'chord giu 8 note');
assert(near(sum, 8 * 0.3 / Math.sqrt(8)), 'tong peak ' + sum.toFixed(4) + ' ~= ' + (8 * 0.3 / Math.sqrt(8)).toFixed(4));
assert(sum <= 1.0, 'tong peak <= 1.0');
// 2. Solo → giữ nguyên velocity
const solo = _scaleNativePolyphony([{ pitch: 60, start_beat: 0, duration_beats: 1, velocity: 0.8 }]);
assert(near(solo[0].velocity, 0.8), 'solo velocity giu nguyen 0.8');
// 3. Dedup: cùng pitch + start_beat → gộp, velocity max
const dup = _cleanNativeNotes([
{ pitch: 60, start_beat: 1, duration_beats: 2, velocity: 0.5 },
{ pitch: 60, start_beat: 1, duration_beats: 3, velocity: 0.7 },
]);
assert(dup.length === 1, 'dedup gop 2 note');
assert(near(dup[0].velocity, 0.7), 'dedup giu velocity cao 0.7');
assert(near(dup[0].duration_beats, 3), 'dedup giu duration dai nhat');
// 4. Legato: note trước cắt 0.01 beats trước note sau cùng pitch
const leg = _cleanNativeNotes([
{ pitch: 60, start_beat: 0, duration_beats: 4, velocity: 0.8 },
{ pitch: 60, start_beat: 2, duration_beats: 1, velocity: 0.8 },
]);
assert(leg.length === 2, 'legato giu 2 note');
assert(near(leg[0].duration_beats, 2 - 0.01, 1e-6), 'note dau cat con ' + leg[0].duration_beats);
assert(near(leg[1].duration_beats, 1), 'note sau giu nguyen');
// 5. Nối tiếp khác pitch → không cắt
const seq = _cleanNativeNotes([
{ pitch: 60, start_beat: 0, duration_beats: 4, velocity: 0.8 },
{ pitch: 64, start_beat: 2, duration_beats: 1, velocity: 0.8 },
]);
assert(near(seq[0].duration_beats, 4), 'khac pitch khong bi cat');
console.log(fails ? 'FAILED: ' + fails : 'ALL PASS');
process.exit(fails ? 1 : 0);
+151
View File
@@ -0,0 +1,151 @@
# Test VST2 host: create SHM, spawn bridge (stdout->file), LOAD_INSTRUMENT (VST2),
# noteOn, read ring audio energy. Verify [Vst2Instrument] loaded log.
import ctypes, subprocess, sys, time, os, tempfile
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
SHM_NAME = 'SonicForge_DAW_IPC_V2TEST'
SHM_SIZE = 30000
PLUGIN = r'C:/Users/locpham/SonicForgeStudio/app/storage/soundfonts/518e850f-a5d3-4790-b1f9-0c90c203c524.sf2'
BRIDGE = r'C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe'
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
k32.CreateFileMappingA.restype = ctypes.c_void_p
k32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_char_p]
k32.MapViewOfFile.restype = ctypes.c_void_p
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
k32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
k32.CloseHandle.argtypes = [ctypes.c_void_p]
h = k32.CreateFileMappingA(ctypes.c_void_p(-1), None, 0x04, 0, SHM_SIZE, SHM_NAME.encode())
if not h:
print('FATAL CreateFileMappingA', ctypes.get_last_error()); sys.exit(1)
ptr = k32.MapViewOfFile(h, 0x02, 0, 0, 0)
if not ptr:
print('FATAL MapViewOfFile', ctypes.get_last_error()); sys.exit(1)
def w32(off, val): ctypes.c_uint32.from_address(ptr + off).value = val
def r32(off): return ctypes.c_uint32.from_address(ptr + off).value
def energy(off, n=256):
arr = (ctypes.c_float * n).from_address(ptr + off)
return sum(abs(v) for v in arr)
OFF_MQ = 2064; OFF_MQ_COUNT = 2832
OFF_CQ = 2836; OFF_CQ_COUNT = 11156
OFF_HEARTBEAT = 11164
MIDI_ENTRY = 12; CTRL_ENTRY = 1040
OFF_RING_L = 11172; OFF_RING_R = 11172 + 8192
OFF_RING_MASK = 11168
def send_ctrl(type_, arg0, arg1, channel, path=b''):
idx = r32(OFF_CQ_COUNT)
if idx >= 8: print('FATAL ctrl full'); sys.exit(1)
base = ptr + OFF_CQ + idx * CTRL_ENTRY
ctypes.c_uint32.from_address(base + 0).value = type_
ctypes.c_uint32.from_address(base + 4).value = arg0
ctypes.c_uint32.from_address(base + 8).value = arg1
ctypes.c_uint32.from_address(base + 12).value = channel
if path:
ctypes.memmove(base + 16, path, len(path))
ctypes.c_char.from_address(base + 16 + len(path)).value = b'\0'
w32(OFF_CQ_COUNT, idx + 1)
print(f'sent ctrl type={type_} arg0={arg0} arg1={arg1} ch={channel}')
def send_midi(cmd, channel, pitch, velocity, live=1, d2=0, d3=0):
idx = r32(OFF_MQ_COUNT)
if idx >= 64: print('FATAL midi full'); sys.exit(1)
base = ptr + OFF_MQ + idx * MIDI_ENTRY
ctypes.c_uint8.from_address(base + 0).value = cmd
ctypes.c_uint8.from_address(base + 1).value = channel
ctypes.c_uint8.from_address(base + 2).value = pitch
ctypes.c_uint8.from_address(base + 3).value = velocity
ctypes.c_uint8.from_address(base + 4).value = d2
ctypes.c_uint8.from_address(base + 5).value = d3
ctypes.c_uint8.from_address(base + 6).value = 1 if live else 0
ctypes.c_uint32.from_address(base + 8).value = 0
w32(OFF_MQ_COUNT, idx + 1)
print(f'sent midi cmd=0x{cmd:x} ch={channel} p={pitch} v={velocity}')
env = dict(os.environ)
env['SF_SHM_NAME'] = SHM_NAME
env['SF_SAMPLE_RATE'] = '44100'
env['SF_BLOCK_SIZE'] = '256'
env['SF_PARENT_PID'] = str(os.getpid())
empty_state = os.path.join(tempfile.gettempdir(), 'sf_v2test_state.json')
open(empty_state, 'w').write('{}')
env['SF_STATE_FILE'] = empty_state
bout = tempfile.NamedTemporaryFile(delete=False, suffix='.log', mode='w', encoding='utf-8')
bout_path = bout.name
bout.close()
with open(bout_path, 'w', encoding='utf-8') as f:
proc = subprocess.Popen([BRIDGE, '--shm', SHM_NAME], env=env,
stdout=f, stderr=subprocess.STDOUT)
print('bridge pid', proc.pid)
time.sleep(2.5)
def tail_bridge(tag):
with open(bout_path, encoding='utf-8', errors='replace') as f:
lines = f.read().splitlines()
print(f'--- bridge {tag} ({len(lines)} lines) ---')
for l in lines[-50:]:
print(l)
def head_bridge(tag, n=60):
with open(bout_path, encoding='utf-8', errors='replace') as f:
lines = f.read().splitlines()
print(f'--- bridge HEAD {tag} ({len(lines)} lines) ---')
for l in lines[:n]:
print(l)
# LOAD VST2 (type enum: VST2=1)
send_ctrl(2, 2, len(PLUGIN.encode()), 0, PLUGIN.encode())
print('proc alive after load:', proc.poll() is None)
time.sleep(2.0)
head_bridge('after LOAD')
tail_bridge('after LOAD')
# TRANSPORT PLAY so plugins gating DSP on transport start producing sound
send_ctrl(3, 1, 0, 0)
time.sleep(0.3)
# CC7 volume max (some presets default to 0 volume).
# NOTE: bridge reads CC value from data2 (matches app.jsx pushEvent data2).
send_midi(0xB, 0, 7, 0, d2=127)
time.sleep(0.2)
send_midi(0x9, 0, 60, 100)
print('mq count after noteOn send:', r32(OFF_MQ_COUNT))
time.sleep(0.5)
print('mq count 0.5s later:', r32(OFF_MQ_COUNT))
for t in (0.5, 1.0, 2.0, 4.0):
time.sleep(t)
print('energy at +', t, ':', round(max(sum(abs(v) for v in (ctypes.c_float * 256).from_address(ptr + OFF_RING_L + s * 1024)) + sum(abs(v) for v in (ctypes.c_float * 256).from_address(ptr + OFF_RING_R + s * 1024)) for s in range(8)), 3))
mask = r32(OFF_RING_MASK) or 7
wi = r32(4)
slots = [(wi - i) & mask for i in range(mask + 1)]
energies = []
for s in slots:
e = energy(OFF_RING_L + s * 1024) + energy(OFF_RING_R + s * 1024)
energies.append(e)
print('ringMask', mask, 'writeIndex', wi, 'heartbeat', r32(OFF_HEARTBEAT))
print('slot energies (L+R):', [round(e, 3) for e in energies])
print('max energy:', round(max(energies), 3))
send_midi(0x8, 0, 60, 0)
time.sleep(0.5)
tail_bridge('tail')
proc.terminate()
time.sleep(1)
if proc.poll() is None:
proc.kill()
k32.UnmapViewOfFile(ptr); k32.CloseHandle(h)
print('--- grep Vst2Instrument in log ---')
with open(bout_path, encoding='utf-8', errors='replace') as f:
hits = [l.rstrip() for l in f if 'Vst2Instrument' in l]
for l in hits[-20:]:
print(l)
import shutil
shutil.copy(bout_path, r'C:/Users/locpham/SonicForgeStudio/tools/_bridge_keep.log')
os.remove(bout_path)
print('done')
+214
View File
@@ -0,0 +1,214 @@
# Test VST2 64-bit GUI: create SHM, spawn bridge, LOAD DUNE 3 (VST2),
# OPEN_GUI (bridge auto-creates native window), verify log "GUI attached",
# EnumWindows finds the host window + editor child, noteOn -> ring energy.
import ctypes, subprocess, sys, time, os, tempfile
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
SHM_NAME = 'SonicForge_DAW_IPC_V2GUITEST'
SHM_SIZE = 30000
PLUGIN = sys.argv[1] if len(sys.argv) > 1 else r'C:\Program Files\Steinberg\VSTPlugins\DUNE 3.dll'
BRIDGE = r'C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe'
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
k32.CreateFileMappingA.restype = ctypes.c_void_p
k32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_char_p]
k32.MapViewOfFile.restype = ctypes.c_void_p
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
k32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
k32.CloseHandle.argtypes = [ctypes.c_void_p]
user32 = ctypes.WinDLL('user32', use_last_error=True)
user32.EnumWindows.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
user32.GetWindowThreadProcessId.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint32)]
user32.GetClassNameA.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int]
user32.GetWindowTextA.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int]
user32.EnumChildWindows.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
user32.PostMessageA.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p, ctypes.c_void_p]
h = k32.CreateFileMappingA(ctypes.c_void_p(-1), None, 0x04, 0, SHM_SIZE, SHM_NAME.encode())
if not h:
print('FATAL CreateFileMappingA', ctypes.get_last_error()); sys.exit(1)
ptr = k32.MapViewOfFile(h, 0x02, 0, 0, 0)
if not ptr:
print('FATAL MapViewOfFile', ctypes.get_last_error()); sys.exit(1)
def w32(off, val): ctypes.c_uint32.from_address(ptr + off).value = val
def r32(off): return ctypes.c_uint32.from_address(ptr + off).value
OFF_MQ = 2064; OFF_MQ_COUNT = 2832
OFF_CQ = 2836; OFF_CQ_COUNT = 11156
OFF_HEARTBEAT = 11164
MIDI_ENTRY = 12; CTRL_ENTRY = 1040
OFF_RING_L = 11172; OFF_RING_R = 11172 + 8192
OFF_RING_MASK = 11168
def send_ctrl(type_, arg0, arg1, channel, path=b''):
idx = r32(OFF_CQ_COUNT)
if idx >= 8: print('FATAL ctrl full'); sys.exit(1)
base = ptr + OFF_CQ + idx * CTRL_ENTRY
ctypes.c_uint32.from_address(base + 0).value = type_
ctypes.c_uint32.from_address(base + 4).value = arg0
ctypes.c_uint32.from_address(base + 8).value = arg1
ctypes.c_uint32.from_address(base + 12).value = channel
if path:
ctypes.memmove(base + 16, path, len(path))
ctypes.c_char.from_address(base + 16 + len(path)).value = b'\0'
w32(OFF_CQ_COUNT, idx + 1)
print(f'sent ctrl type={type_} arg0={arg0} arg1={arg1} ch={channel}')
def send_midi(cmd, channel, pitch, velocity, live=1, d2=0, d3=0):
idx = r32(OFF_MQ_COUNT)
if idx >= 64: print('FATAL midi full'); sys.exit(1)
base = ptr + OFF_MQ + idx * MIDI_ENTRY
ctypes.c_uint8.from_address(base + 0).value = cmd
ctypes.c_uint8.from_address(base + 1).value = channel
ctypes.c_uint8.from_address(base + 2).value = pitch
ctypes.c_uint8.from_address(base + 3).value = velocity
ctypes.c_uint8.from_address(base + 4).value = d2
ctypes.c_uint8.from_address(base + 5).value = d3
ctypes.c_uint8.from_address(base + 6).value = 1 if live else 0
ctypes.c_uint32.from_address(base + 8).value = 0
w32(OFF_MQ_COUNT, idx + 1)
print(f'sent midi cmd=0x{cmd:x} ch={channel} p={pitch} v={velocity}')
env = dict(os.environ)
env['SF_SHM_NAME'] = SHM_NAME
env['SF_SAMPLE_RATE'] = '44100'
env['SF_BLOCK_SIZE'] = '256'
env['SF_PARENT_PID'] = str(os.getpid())
empty_state = os.path.join(tempfile.gettempdir(), 'sf_v2guittest_state.json')
open(empty_state, 'w').write('{}')
env['SF_STATE_FILE'] = empty_state
bout_path = os.path.join(tempfile.gettempdir(), 'sf_v2guittest_bridge.log')
with open(bout_path, 'w', encoding='utf-8') as f:
proc = subprocess.Popen([BRIDGE, '--shm', SHM_NAME], env=env,
stdout=f, stderr=subprocess.STDOUT)
print('bridge pid', proc.pid)
time.sleep(2.5)
def tail_bridge(tag):
with open(bout_path, encoding='utf-8', errors='replace') as f:
lines = f.read().splitlines()
print(f'--- bridge {tag} ({len(lines)} lines) ---')
for l in lines[-40:]:
print(l)
def dump_modules(bridge_pid):
# module base..end ranges of the bridge process (CreateToolhelp32Snapshot)
k32t = ctypes.WinDLL('kernel32', use_last_error=True)
class MODULEENTRY32W(ctypes.Structure):
_fields_ = [('dwSize', ctypes.c_uint32), ('th32ModuleID', ctypes.c_uint32),
('th32ProcessID', ctypes.c_uint32), ('GlblcntUsage', ctypes.c_uint32),
('ProccntUsage', ctypes.c_uint32), ('modBaseAddr', ctypes.c_void_p),
('modBaseSize', ctypes.c_uint32), ('hModule', ctypes.c_void_p),
('szModule', ctypes.c_wchar * 256), ('szExePath', ctypes.c_wchar * 260)]
k32t.CreateToolhelp32Snapshot.restype = ctypes.c_void_p
k32t.CreateToolhelp32Snapshot.argtypes = [ctypes.c_uint32, ctypes.c_uint32]
k32t.Module32FirstW.argtypes = [ctypes.c_void_p, ctypes.POINTER(MODULEENTRY32W)]
k32t.Module32NextW.argtypes = [ctypes.c_void_p, ctypes.POINTER(MODULEENTRY32W)]
snap = k32t.CreateToolhelp32Snapshot(0x8, bridge_pid) # TH32CS_SNAPMODULE
if snap == ctypes.c_void_p(-1).value or not snap:
print('module snapshot failed', ctypes.get_last_error()); return []
me = MODULEENTRY32W(); me.dwSize = ctypes.sizeof(MODULEENTRY32W)
mods = []
if k32t.Module32FirstW(snap, ctypes.byref(me)):
while True:
base = me.modBaseAddr or 0
mods.append((base, base + me.modBaseSize, me.szModule))
if not k32t.Module32NextW(snap, ctypes.byref(me)):
break
k32t.CloseHandle(snap)
return mods
def dump_windows(bridge_pid):
found = []
@ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
def cb(hwnd, lparam):
pid = ctypes.c_uint32()
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
if pid.value == bridge_pid:
cls = ctypes.create_string_buffer(128)
user32.GetClassNameA(hwnd, cls, 128)
title = ctypes.create_string_buffer(256)
user32.GetWindowTextA(hwnd, title, 256)
found.append((hwnd, cls.value.decode('latin1'), title.value.decode('latin1', 'replace')))
return 1
user32.EnumWindows(cb, 0)
print(f'--- top-level windows of pid {bridge_pid}: {len(found)} ---')
for hwnd, cls, title in found:
print(f' hwnd={hwnd:#x} class={cls!r} title={title!r}')
# enumerate children of this window
kids = []
@ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
def cbk(hw, lp):
c2 = ctypes.create_string_buffer(128)
user32.GetClassNameA(hw, c2, 128)
t2 = ctypes.create_string_buffer(256)
user32.GetWindowTextA(hw, t2, 256)
kids.append((hw, c2.value.decode('latin1'), t2.value.decode('latin1', 'replace')))
return 1
user32.EnumChildWindows(hwnd, cbk, 0)
for kh, kc, kt in kids:
print(f' child hwnd={kh:#x} class={kc!r} title={kt!r}')
return found
# LOAD VST2 (type enum: VST2=1)
send_ctrl(2, 1, len(PLUGIN.encode()), 0, PLUGIN.encode())
print('proc alive after load:', proc.poll() is None)
time.sleep(2.0)
tail_bridge('after LOAD')
pbase = os.path.basename(PLUGIN).lower().replace('.dll', '')
print('--- bridge modules (plugin-related) ---')
for b, e, name in dump_modules(proc.pid):
nl = name.lower()
if 'sonicforge' in nl or 'daw_vst' in nl or pbase in nl:
print(f' {b:#x}..{e:#x} {name}')
# OPEN_GUI: type=4, arg1=0 (bridge creates native window), arg2=pluginId
send_ctrl(4, 0, 0, 0, PLUGIN.encode())
time.sleep(3.0)
tail_bridge('after OPEN_GUI')
print('proc alive after open_gui:', proc.poll() is None)
wins = dump_windows(proc.pid)
host = [w for w in wins if 'SonicForge_Native_VST3_Class' in w[1]]
print('host window found:', len(host) > 0)
if host:
child_count = 0
for w in wins:
pass
# count editor children under the host window
kids = []
@ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
def cbk2(hw, lp):
c2 = ctypes.create_string_buffer(128)
user32.GetClassNameA(hw, c2, 128)
kids.append((hw, c2.value.decode('latin1')))
return 1
user32.EnumChildWindows(host[0][0], cbk2, 0)
print('editor child windows under host:', len(kids), [k[1] for k in kids])
# TRANSPORT PLAY + note -> energy
send_ctrl(3, 1, 0, 0)
time.sleep(0.3)
send_midi(0xB, 0, 7, 127)
time.sleep(0.2)
send_midi(0x9, 0, 60, 100)
for t in (0.5, 1.0, 2.0):
time.sleep(t)
mask = r32(OFF_RING_MASK) or 7
wi = r32(4)
slots = [(wi - i) & mask for i in range(mask + 1)]
es = []
for s in slots:
e = sum(abs(v) for v in (ctypes.c_float * 256).from_address(ptr + OFF_RING_L + s * 1024)) + \
sum(abs(v) for v in (ctypes.c_float * 256).from_address(ptr + OFF_RING_R + s * 1024))
es.append(e)
print(f'energy at +{t}: max={round(max(es), 3)} heartbeat={r32(OFF_HEARTBEAT)}')
print('proc alive at end:', proc.poll() is None)
proc.terminate()
time.sleep(0.5)
k32.UnmapViewOfFile(ptr)
k32.CloseHandle(h)
print('DONE')
+137
View File
@@ -0,0 +1,137 @@
# Test VST2 host: create SHM, spawn bridge (stdout->file), LOAD_INSTRUMENT (VST2),
# noteOn, read ring audio energy. Verify [Vst2Instrument] loaded log.
import ctypes, subprocess, sys, time, os, tempfile
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
SHM_NAME = 'SonicForge_DAW_IPC_V2TEST'
SHM_SIZE = 30000
PLUGIN = r'C:\Program Files\Steinberg\VSTPlugins\DUNE 3.dll'
BRIDGE = r'C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe'
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
k32.CreateFileMappingA.restype = ctypes.c_void_p
k32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_char_p]
k32.MapViewOfFile.restype = ctypes.c_void_p
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
k32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
k32.CloseHandle.argtypes = [ctypes.c_void_p]
h = k32.CreateFileMappingA(ctypes.c_void_p(-1), None, 0x04, 0, SHM_SIZE, SHM_NAME.encode())
if not h:
print('FATAL CreateFileMappingA', ctypes.get_last_error()); sys.exit(1)
ptr = k32.MapViewOfFile(h, 0x02, 0, 0, 0)
if not ptr:
print('FATAL MapViewOfFile', ctypes.get_last_error()); sys.exit(1)
def w32(off, val): ctypes.c_uint32.from_address(ptr + off).value = val
def r32(off): return ctypes.c_uint32.from_address(ptr + off).value
def energy(off, n=256):
arr = (ctypes.c_float * n).from_address(ptr + off)
return sum(abs(v) for v in arr)
OFF_MQ = 2064; OFF_MQ_COUNT = 2832
OFF_CQ = 2836; OFF_CQ_COUNT = 11156
OFF_HEARTBEAT = 11164
MIDI_ENTRY = 12; CTRL_ENTRY = 1040
OFF_RING_L = 11172; OFF_RING_R = 11172 + 8192
OFF_RING_MASK = 11168
def send_ctrl(type_, arg0, arg1, channel, path=b''):
idx = r32(OFF_CQ_COUNT)
if idx >= 8: print('FATAL ctrl full'); sys.exit(1)
base = ptr + OFF_CQ + idx * CTRL_ENTRY
ctypes.c_uint32.from_address(base + 0).value = type_
ctypes.c_uint32.from_address(base + 4).value = arg0
ctypes.c_uint32.from_address(base + 8).value = arg1
ctypes.c_uint32.from_address(base + 12).value = channel
if path:
ctypes.memmove(base + 16, path, len(path))
ctypes.c_char.from_address(base + 16 + len(path)).value = b'\0'
w32(OFF_CQ_COUNT, idx + 1)
print(f'sent ctrl type={type_} arg0={arg0} arg1={arg1} ch={channel}')
def send_midi(cmd, channel, pitch, velocity, live=1, d2=0, d3=0):
idx = r32(OFF_MQ_COUNT)
if idx >= 64: print('FATAL midi full'); sys.exit(1)
base = ptr + OFF_MQ + idx * MIDI_ENTRY
ctypes.c_uint8.from_address(base + 0).value = cmd
ctypes.c_uint8.from_address(base + 1).value = channel
ctypes.c_uint8.from_address(base + 2).value = pitch
ctypes.c_uint8.from_address(base + 3).value = velocity
ctypes.c_uint8.from_address(base + 4).value = d2
ctypes.c_uint8.from_address(base + 5).value = d3
ctypes.c_uint8.from_address(base + 6).value = 1 if live else 0
ctypes.c_uint32.from_address(base + 8).value = 0
w32(OFF_MQ_COUNT, idx + 1)
print(f'sent midi cmd=0x{cmd:x} ch={channel} p={pitch} v={velocity}')
env = dict(os.environ)
env['SF_SHM_NAME'] = SHM_NAME
env['SF_SAMPLE_RATE'] = '44100'
env['SF_BLOCK_SIZE'] = '256'
env['SF_PARENT_PID'] = str(os.getpid())
empty_state = os.path.join(tempfile.gettempdir(), 'sf_v2test_state.json')
open(empty_state, 'w').write('{}')
env['SF_STATE_FILE'] = empty_state
bout = tempfile.NamedTemporaryFile(delete=False, suffix='.log', mode='w', encoding='utf-8')
bout_path = bout.name
bout.close()
with open(bout_path, 'w', encoding='utf-8') as f:
proc = subprocess.Popen([BRIDGE, '--shm', SHM_NAME], env=env,
stdout=f, stderr=subprocess.STDOUT)
print('bridge pid', proc.pid)
time.sleep(2.5)
def tail_bridge(tag):
with open(bout_path, encoding='utf-8', errors='replace') as f:
lines = f.read().splitlines()
print(f'--- bridge {tag} ({len(lines)} lines) ---')
for l in lines[-50:]:
print(l)
# LOAD VST2 (type enum: VST2=1)
send_ctrl(2, 1, len(PLUGIN.encode()), 0, PLUGIN.encode())
print('proc alive after load:', proc.poll() is None)
time.sleep(2.0)
tail_bridge('after LOAD')
# TRANSPORT PLAY so plugins gating DSP on transport start producing sound
send_ctrl(3, 1, 0, 0)
time.sleep(0.3)
# CC7 volume max (some presets default to 0 volume)
send_midi(0xB, 0, 7, 127)
time.sleep(0.2)
send_midi(0x9, 0, 60, 100)
for t in (0.5, 1.0, 2.0, 4.0):
time.sleep(t)
print('energy at +', t, ':', round(max(sum(abs(v) for v in (ctypes.c_float * 256).from_address(ptr + OFF_RING_L + s * 1024)) + sum(abs(v) for v in (ctypes.c_float * 256).from_address(ptr + OFF_RING_R + s * 1024)) for s in range(8)), 3))
mask = r32(OFF_RING_MASK) or 7
wi = r32(4)
slots = [(wi - i) & mask for i in range(mask + 1)]
energies = []
for s in slots:
e = energy(OFF_RING_L + s * 1024) + energy(OFF_RING_R + s * 1024)
energies.append(e)
print('ringMask', mask, 'writeIndex', wi, 'heartbeat', r32(OFF_HEARTBEAT))
print('slot energies (L+R):', [round(e, 3) for e in energies])
print('max energy:', round(max(energies), 3))
send_midi(0x8, 0, 60, 0)
time.sleep(0.5)
tail_bridge('tail')
proc.terminate()
time.sleep(1)
if proc.poll() is None:
proc.kill()
k32.UnmapViewOfFile(ptr); k32.CloseHandle(h)
print('--- grep Vst2Instrument in log ---')
with open(bout_path, encoding='utf-8', errors='replace') as f:
hits = [l.rstrip() for l in f if 'Vst2Instrument' in l]
for l in hits[-20:]:
print(l)
os.remove(bout_path)
print('done')
+69
View File
@@ -0,0 +1,69 @@
import struct
path = r"C:/Users/locpham/AppData/Local/Temp/sf_dumps/bridge_crash_1696.dmp"
data = open(path, "rb").read()
sig, ver, numstreams, sdir = struct.unpack_from("<IIIIIIQ", data, 0)[:4]
streams = {}
for i in range(numstreams):
stype, datasize, rva = struct.unpack_from("<III", data, sdir + i * 12)
streams[stype] = (rva, datasize)
mods = []
rva, sz = streams[4]
n = struct.unpack_from("<I", data, rva)[0]
pos = rva + 4
for i in range(n):
base, imgsz, cksum, ts, name_rva = struct.unpack_from("<QIIII", data, pos)
nlen = struct.unpack_from("<I", data, name_rva)[0]
name = data[name_rva + 4:name_rva + 4 + nlen].decode("utf-16-le", "replace").rstrip("\x00")
mods.append((base, imgsz, name))
pos += 108
def mod_for(addr):
for base, szz, name in mods:
if base <= addr < base + szz:
return base, name.split("\\")[-1]
return None, None
# crash thread context
rva, sz = streams[6]
ex_tid = struct.unpack_from("<I", data, rva)[0]
rva, sz = streams[3]
n = struct.unpack_from("<I", data, rva)[0]
pos = rva + 4
ctx_rva = None
for i in range(n):
tid2, susp, pric, pri, teb, st_start, st_sz, st_rva, ctx_sz2, ctx_rva2 = struct.unpack_from("<IIIIQQIIII", data, pos)
if tid2 == ex_tid:
ctx_rva = ctx_rva2
st_rva_used = st_rva
st_start_used = st_start
pos += 48
rip = struct.unpack_from("<Q", data, ctx_rva + 0xF8)[0]
rsp = struct.unpack_from("<Q", data, ctx_rva + 0x98)[0]
print("crash tid", ex_tid, "RIP", hex(rip), "RSP", hex(rsp))
# find memory region covering RSP
rva, sz = streams[5]
n = struct.unpack_from("<I", data, rva)[0]
pos = rva + 4
region = None
for i in range(n):
start, msz, mrva = struct.unpack_from("<QII", data, pos)
if start <= rsp < start + msz:
region = (start, msz, mrva)
pos += 16
print("stack region", region and (hex(region[0]), hex(region[1])))
# walk stack: assume return addresses are qwords that fall into executable modules
start, msz, mrva = region
base_rsp = rsp
print("\nstack walk from RSP:")
prev = None
for j in range(0, 16384 - 8, 8):
addr = base_rsp + j
if addr >= start + msz: break
val = struct.unpack_from("<Q", data, mrva + (addr - start))[0]
b, nm = mod_for(val)
if nm and (val - b) > 0x1000: # code pointer heuristic (offset beyond header)
print(" %016x [%04x] %016x -> %s+%x" % (addr, j, val, nm, val - b))