# 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" "
\n" " \n" " {idx + 1}\n" " \n" " {s.preset_b64 ? '✓ preset' : 'no preset'}\n" " \n" " \n" " \n" " \n" " \n" "
\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" "
\n" "
\n" "
\n" " VST FX:\n" " Plugin VST3 (Ozone...) — realtime browser vẫn WebAudio builtin; VST áp dụng lúc Export/Offline render (preview = âm export)\n" "
\n" "
\n" " \n" " \n" "
\n" "
\n" " {vstChain.length === 0 &&
Chưa có VST FX — chọn plugin bên dưới để thêm vào insert chain (WebAudio chain → VST FX → Fader).
}\n" " {vstChain.map((s, idx) => vstRow(s, idx, toggleVst, changeVstPlugin, uploadVstPreset, moveVst, removeVst, fxList, vstChain.length))}\n" " \n" "
\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" "
\n" "
\n" "
\n" " VST FX MASTERBUS:\n" " Plugin VST3 (Ozone...) — realtime browser vẫn WebAudio builtin; VST áp dụng lúc Export/Offline render (preview = âm export)\n" "
\n" "
\n" " \n" " \n" "
\n" "
\n" " {masterVstChain.length === 0 &&
Chưa có VST FX masterbus — chọn plugin bên dưới (Ozone...) để chạy chain sau khi mix WebAudio.
}\n" " {masterVstChain.map((s, idx) => vstRow(s, idx, toggleMasterVst, changeMasterVstPlugin, uploadMasterVstPreset, moveMasterVst, removeMasterVst, masterFxList, masterVstChain.length))}\n" " \n" "
\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()