Files
SonicForgeStudio/tools/patch_app_jsx_phase3.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

346 lines
23 KiB
Python

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