diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 6f7f2c2..8cac59c 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -845,6 +845,7 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, close_fds=os.name != "nt", + env=dict(os.environ, SF_PARENT_PID=str(os.getpid())), creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0) # Bridge prints SF_FXGUI_PORT= after binding its HTTP server. embed_url = None @@ -870,6 +871,7 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren return {"success": True, "started": True, "already_running": False, "embed_url": embed_url, "cmd": cmd} proc = subprocess.Popen(cmd, close_fds=os.name != "nt", + env=dict(os.environ, SF_PARENT_PID=str(os.getpid())), creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0) proc._sf_plugin_path = plugin_path _register_fx_gui_process(proc) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index cade889..40e8368 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -12435,7 +12435,7 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => { fxGuiOpenRef.current = fxGuiOpen; const closeFxGui = () => { const g = fxGuiOpenRef.current; - if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} } + if (g) { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} } setFxGuiOpen(null); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: null, vstGuiOpenName: null }); }; @@ -12462,7 +12462,7 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => { const toggleVst = (idx) => setVstChain(vstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s)); const removeVst = (idx) => { const rem = vstChain[idx]; if (rem && fxGuiOpen && fxGuiOpen.path === rem.path) closeFxGui(); if (rem) delete _capturedPresetByPath[rem.path]; if (idx === activeVstIdx) setActiveVstIdx(-1); setVstChain(vstChain.filter((_, i) => i !== idx)); }; 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); }; - const openVstGui = (s, idx) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (fxGuiOpen) closeFxGui(); if (idx !== undefined) setActiveVstIdx(idx); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); }; + const openVstGui = (s, idx) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (fxGuiOpen) closeFxGui(); if (idx !== undefined) setActiveVstIdx(idx); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); }; const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path); if (!fx) return; const next = [...vstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]; setVstChain(next); setActiveVstIdx(next.length - 1); }; const changeVstPlugin = (idx, path) => { const fx = (fxList || []).find(f => f.path === path); delete _capturedPresetByPath[path]; setVstChain(vstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); }; const uploadVstPreset = (idx) => { @@ -13029,7 +13029,7 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin masterGuiOpenRef.current = masterGuiOpen; const closeMasterGui = () => { const g = masterGuiOpenRef.current; - if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} } + if (g) { try { fetch(g.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} } setMasterGuiOpen(null); setActiveMasterVstPath(null); setOzState(prev => { const n = { ...(prev || {}) }; delete n.gui_open_path; delete n.gui_open_name; return n; }); @@ -13055,17 +13055,14 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen, ozState && ozState.gui_open_path]); const setMasterVstChain = (next) => setOzState(prev => ({ ...prev, vstFxChain: next })); - // T2.4: push the master VST FX chain to the realtime bridge (live master - // mix). v1: master chain only — per-track vstChain stays offline (defer). - React.useEffect(() => { - if (!window.NativeBridgeService || !window.NativeBridgeService.setFxChain) return; - const chain = (ozState.vstFxChain || []).map(s => ({ path: s.path || '', bypass: !!s.bypass, preset_b64: s.preset_b64 || '' })); - window.NativeBridgeService.setFxChain(JSON.stringify(chain)); - }, [ozState.vstFxChain]); + // LIVE master FX xử lý bởi fxRtStart() (SHM loop qua fx_vst_bridge + // --realtime-fx) — KHÔNG push chain xuống native bridge (setFxChain): + // nếu push, MIDI/VSTi instrument bị xử lý 2 lần (native bridge + SHM loop) + // → over-process, bể/crackle. setFxChain vẫn dùng khi Export/Offline render. const toggleMasterVst = (idx) => setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s)); const removeMasterVst = (idx) => { const rem = masterVstChain[idx]; if (rem) delete _capturedPresetByPath[rem.path]; setMasterVstChain(masterVstChain.filter((_, i) => i !== idx)); }; 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); }; - const openMasterVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (masterGuiOpen) closeMasterGui(); if (s.path) setActiveMasterVstPath(s.path); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); }; + const openMasterVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (masterGuiOpen) closeMasterGui(); if (s.path) setActiveMasterVstPath(s.path); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); }; 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 }]); openMasterVstGui({ path: fx.path, name: fx.name || fx.id }); }; const changeMasterVstPlugin = (idx, path) => { const fx = (masterFxList || []).find(f => f.path === path); delete _capturedPresetByPath[path]; setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); }; diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index df14a12..73403c2 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -830,11 +830,11 @@ const[fxList,setFxList]=React.useState([]);const[fxLoading,setFxLoading]=React.u const vstChain=track.vstFxChain||[];// Unified chain strip: builtin DSP + VST FX chung 1 chain (kéo đổi thứ tự giữa cả 2 loại). const unifiedChain=[...chain.map((m,i)=>({kind:'builtin',m,i})),...vstChain.map((s,i)=>({kind:'vst',s,i}))];const setVstChain=next=>{if(onUpdateTrack)onUpdateTrack(track.id,{vstFxChain:next,vstFxBypass:vstBypass});if(window.__rebuildTrackFxGraph)window.__rebuildTrackFxGraph(track.id,{vstFxChain:next,vstFxBypass:vstBypass});};// Embedded VST GUI state (PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 3) const[fxGuiOpen,setFxGuiOpenState]=React.useState(_persistFxGuiOpen);// { path, url } | null -const setFxGuiOpen=v=>{_persistFxGuiOpen=v;_persistFxGuiTrackId=v?track&&track.id:null;setFxGuiOpenState(v);};const fxGuiOpenRef=React.useRef(null);fxGuiOpenRef.current=fxGuiOpen;const closeFxGui=()=>{const g=fxGuiOpenRef.current;if(g){try{fetch(g.url+'/hide',{method:'POST'}).catch(()=>{});}catch(e){}}setFxGuiOpen(null);if(onUpdateTrack&&track)onUpdateTrack(track.id,{vstGuiOpenPath:null,vstGuiOpenName:null});};React.useEffect(()=>()=>{const g=fxGuiOpenRef.current;if(g){try{fetch(g.url+'/hide',{method:'POST'}).catch(()=>{});}catch(e){}}},[]);// Capture GUI knob tweaks while the embedded GUI is open (bridge /preset). +const setFxGuiOpen=v=>{_persistFxGuiOpen=v;_persistFxGuiTrackId=v?track&&track.id:null;setFxGuiOpenState(v);};const fxGuiOpenRef=React.useRef(null);fxGuiOpenRef.current=fxGuiOpen;const closeFxGui=()=>{const g=fxGuiOpenRef.current;if(g){try{fetch(g.url+'/close',{method:'POST'}).catch(()=>{});}catch(e){}}setFxGuiOpen(null);if(onUpdateTrack&&track)onUpdateTrack(track.id,{vstGuiOpenPath:null,vstGuiOpenName:null});};React.useEffect(()=>()=>{const g=fxGuiOpenRef.current;if(g){try{fetch(g.url+'/hide',{method:'POST'}).catch(()=>{});}catch(e){}}},[]);// Capture GUI knob tweaks while the embedded GUI is open (bridge /preset). React.useEffect(()=>{if(!fxGuiOpen||!fxGuiOpen.url)return;const iv=setInterval(()=>{fetch(fxGuiOpen.url+'/preset').then(r=>r.json()).then(d=>{if(d&&d.preset_b64)_capturedPresetByPath[fxGuiOpen.path]=d.preset_b64;}).catch(()=>{});},1500);return()=>clearInterval(iv);// eslint-disable-next-line react-hooks/exhaustive-deps },[fxGuiOpen]);// Reopen the saved GUI when the rack is mounted after a project restore. React.useEffect(()=>{if(!track||!track.vstGuiOpenPath||fxGuiOpen)return;const s=(track.vstFxChain||[]).find(x=>x.path===track.vstGuiOpenPath);if(!s||!window.SonicAPI||!window.SonicAPI.openFxGui)return;window.SonicAPI.openFxGui({path:s.path,name:s.name||s.path,embed:true}).then(r=>{if(r&&r.embed_url)setFxGuiOpen({path:s.path,url:r.embed_url});}).catch(()=>{});// eslint-disable-next-line react-hooks/exhaustive-deps -},[track&&track.id,track&&track.vstGuiOpenPath]);const toggleVst=idx=>setVstChain(vstChain.map((s,i)=>i===idx?{...s,bypass:!s.bypass}:s));const removeVst=idx=>{const rem=vstChain[idx];if(rem&&fxGuiOpen&&fxGuiOpen.path===rem.path)closeFxGui();if(rem)delete _capturedPresetByPath[rem.path];if(idx===activeVstIdx)setActiveVstIdx(-1);setVstChain(vstChain.filter((_,i)=>i!==idx));};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);};const openVstGui=(s,idx)=>{if(!s||!s.path){window.showToast&&window.showToast('Chọn VST FX trước','warning');return;}if(fxGuiOpen)closeFxGui();if(idx!==undefined)setActiveVstIdx(idx);if(!window.SonicAPI||!window.SonicAPI.openFxGui){window.showToast&&window.showToast('Máy này không hỗ trợ mở GUI VST','warning');return;}window.SonicAPI.openFxGui({path:s.path,name:s.name||s.path,embed:true}).then(r=>{if(r&&r.embed_url){setFxGuiOpen({path:s.path,url:r.embed_url,name:s.name||s.path});if(onUpdateTrack&&track)onUpdateTrack(track.id,{vstGuiOpenPath:s.path,vstGuiOpenName:s.name||s.path});}else if(r&&r.already_running)window.showToast&&window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)','info');else window.showToast&&window.showToast('Không mở được GUI embed','error');}).catch(err=>window.showToast&&window.showToast('Lỗi mở GUI: '+(err.message||err),'error'));};const addVst=path=>{const fx=(fxList||[]).find(f=>f.path===path);if(!fx)return;const next=[...vstChain,{type:'vst3',path:fx.path,name:fx.name||fx.id,preset_b64:'',bypass:false}];setVstChain(next);setActiveVstIdx(next.length-1);};const changeVstPlugin=(idx,path)=>{const fx=(fxList||[]).find(f=>f.path===path);delete _capturedPresetByPath[path];setVstChain(vstChain.map((s,i)=>i===idx?{...s,path,name:fx?fx.name||fx.id:s.name,preset_b64:''}:s));};const uploadVstPreset=idx=>{const inp=document.createElement('input');inp.type='file';inp.accept='.vstpreset,.fxp,.fxb,.dspreset';inp.onchange=()=>{const f=inp.files&&inp.files[0];if(!f)return;const reader=new FileReader();reader.onload=()=>{const b64=String(reader.result||'').split(',')[1]||'';setVstChain(vstChain.map((s,i)=>i===idx?{...s,preset_b64:b64}:s));window.showToast&&window.showToast('Đã gắn preset .vstpreset vào slot '+(idx+1),'success');};reader.readAsDataURL(f);};inp.click();};const previewFx=async()=>{try{if(previewAudioRef.current){previewAudioRef.current.pause();previewAudioRef.current=null;}if(!window.__renderProjectPreview){window.showToast&&window.showToast('Không có hàm render preview','warning');return;}window.showToast&&window.showToast('Đang render offline (bridge)...','info');const res=await window.__renderProjectPreview();const au=new Audio((window.API_BASE_URL||'')+res.url);previewAudioRef.current=au;au.onended=()=>{previewAudioRef.current=null;};au.play().then(()=>window.showToast&&window.showToast('Preview offline — âm thật = âm export (VST FX)','success')).catch(()=>{});}catch(e){/* toast ở __renderProjectPreview */}};const setChain=next=>{if(onUpdateTrack)onUpdateTrack(track.id,{fxChain:next});if(window.__rebuildTrackFxGraph)window.__rebuildTrackFxGraph(track.id,{fxChain:next});};const paramsOf=m=>({...(TRACK_FX_DEFAULTS[m.type]||{}),...(m.params||{})});const setParams=(idx,patch)=>{const next=chain.map((m,i)=>i===idx?{...m,params:{...paramsOf(m),...patch}}:m);setChain(next);};const toggleMod=idx=>{const next=chain.map((m,i)=>i===idx?{...m,active:!(m.active!==false)}:m);setChain(next);};const removeMod=idx=>setChain(chain.filter((_,i)=>i!==idx));const addMod=type=>{const def=TRACK_FX_DEFAULTS[type]||{};// Deep-clone default params so each chain entry owns its data (bands array +},[track&&track.id,track&&track.vstGuiOpenPath]);const toggleVst=idx=>setVstChain(vstChain.map((s,i)=>i===idx?{...s,bypass:!s.bypass}:s));const removeVst=idx=>{const rem=vstChain[idx];if(rem&&fxGuiOpen&&fxGuiOpen.path===rem.path)closeFxGui();if(rem)delete _capturedPresetByPath[rem.path];if(idx===activeVstIdx)setActiveVstIdx(-1);setVstChain(vstChain.filter((_,i)=>i!==idx));};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);};const openVstGui=(s,idx)=>{if(!s||!s.path){window.showToast&&window.showToast('Chọn VST FX trước','warning');return;}if(fxGuiOpen)closeFxGui();if(idx!==undefined)setActiveVstIdx(idx);if(!window.SonicAPI||!window.SonicAPI.openFxGui){window.showToast&&window.showToast('Máy này không hỗ trợ mở GUI VST','warning');return;}window.SonicAPI.openFxGui({path:s.path,name:s.name||s.path,embed:true}).then(r=>{if(r&&r.embed_url){setFxGuiOpen({path:s.path,url:r.embed_url,name:s.name||s.path});if(onUpdateTrack&&track)onUpdateTrack(track.id,{vstGuiOpenPath:s.path,vstGuiOpenName:s.name||s.path});}else if(r&&r.already_running&&r.embed_url){setFxGuiOpen({path:s.path,url:r.embed_url,name:s.name||s.path});if(onUpdateTrack&&track)onUpdateTrack(track.id,{vstGuiOpenPath:s.path,vstGuiOpenName:s.name||s.path});}else if(r&&r.already_running)window.showToast&&window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)','info');else window.showToast&&window.showToast('Không mở được GUI embed','error');}).catch(err=>window.showToast&&window.showToast('Lỗi mở GUI: '+(err.message||err),'error'));};const addVst=path=>{const fx=(fxList||[]).find(f=>f.path===path);if(!fx)return;const next=[...vstChain,{type:'vst3',path:fx.path,name:fx.name||fx.id,preset_b64:'',bypass:false}];setVstChain(next);setActiveVstIdx(next.length-1);};const changeVstPlugin=(idx,path)=>{const fx=(fxList||[]).find(f=>f.path===path);delete _capturedPresetByPath[path];setVstChain(vstChain.map((s,i)=>i===idx?{...s,path,name:fx?fx.name||fx.id:s.name,preset_b64:''}:s));};const uploadVstPreset=idx=>{const inp=document.createElement('input');inp.type='file';inp.accept='.vstpreset,.fxp,.fxb,.dspreset';inp.onchange=()=>{const f=inp.files&&inp.files[0];if(!f)return;const reader=new FileReader();reader.onload=()=>{const b64=String(reader.result||'').split(',')[1]||'';setVstChain(vstChain.map((s,i)=>i===idx?{...s,preset_b64:b64}:s));window.showToast&&window.showToast('Đã gắn preset .vstpreset vào slot '+(idx+1),'success');};reader.readAsDataURL(f);};inp.click();};const previewFx=async()=>{try{if(previewAudioRef.current){previewAudioRef.current.pause();previewAudioRef.current=null;}if(!window.__renderProjectPreview){window.showToast&&window.showToast('Không có hàm render preview','warning');return;}window.showToast&&window.showToast('Đang render offline (bridge)...','info');const res=await window.__renderProjectPreview();const au=new Audio((window.API_BASE_URL||'')+res.url);previewAudioRef.current=au;au.onended=()=>{previewAudioRef.current=null;};au.play().then(()=>window.showToast&&window.showToast('Preview offline — âm thật = âm export (VST FX)','success')).catch(()=>{});}catch(e){/* toast ở __renderProjectPreview */}};const setChain=next=>{if(onUpdateTrack)onUpdateTrack(track.id,{fxChain:next});if(window.__rebuildTrackFxGraph)window.__rebuildTrackFxGraph(track.id,{fxChain:next});};const paramsOf=m=>({...(TRACK_FX_DEFAULTS[m.type]||{}),...(m.params||{})});const setParams=(idx,patch)=>{const next=chain.map((m,i)=>i===idx?{...m,params:{...paramsOf(m),...patch}}:m);setChain(next);};const toggleMod=idx=>{const next=chain.map((m,i)=>i===idx?{...m,active:!(m.active!==false)}:m);setChain(next);};const removeMod=idx=>setChain(chain.filter((_,i)=>i!==idx));const addMod=type=>{const def=TRACK_FX_DEFAULTS[type]||{};// Deep-clone default params so each chain entry owns its data (bands array // especially — shared references would corrupt other modules' state). const params=Array.isArray(def.bands)?{...def,bands:JSON.parse(JSON.stringify(def.bands))}:{...def};setChain([...chain,{type,active:true,params}]);setActiveType(type);setActiveVstIdx(-1);setAddModuleOpen(false);};const applyPreset=key=>{const preset=TRACK_EQ_PRESETS[key];if(!preset)return;const idx=chain.findIndex(m=>m.type==='eq'&&m.active!==false);if(idx>=0)setParams(idx,{g1:preset.g[0],g2:preset.g[1],g3:preset.g[2],g4:preset.g[3]});};const activeMod=chain.find(m=>m.type===activeType)||chain[chain.length-1]||null;const activeIdx=chain.findIndex(m=>m===activeMod);const ap=activeMod?paramsOf(activeMod):{};const slider=(label,val,min,max,step,color,onChange,fmt)=>/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("div",{className:"flex justify-between text-[11px] font-mono"},/*#__PURE__*/React.createElement("span",{style:{color,fontWeight:700}},label),/*#__PURE__*/React.createElement("span",{className:"text-slate-300"},fmt?fmt(val):val)),/*#__PURE__*/React.createElement("input",{type:"range",min:min,max:max,step:step||0.1,value:val,onChange:e=>onChange(parseFloat(e.target.value)),className:"w-full h-1 cursor-pointer",style:{accentColor:color}}));// ── Wave Observer real-time scope render loop ── React.useEffect(()=>{const canvas=scopeCanvasRef.current;if(!canvas||!track)return;const st=scopeStateRef.current;let raf=null;const draw=()=>{raf=requestAnimationFrame(draw);const w=canvas.width,h=canvas.height;const ctx=canvas.getContext('2d');ctx.clearRect(0,0,w,h);// Grid @@ -855,13 +855,15 @@ ctx.fillStyle='#475569';ctx.font='9px monospace';ctx.fillText('+12 dB',4,midY-h/ let _persistMasterGuiOpen=null;const MasteringModal=({isOpen,onClose,masteringSettings,setMasteringSettings})=>{const ozState=masteringSettings;const setOzState=setMasteringSettings;const[isPlaying,setIsPlaying]=React.useState(false);const masterConnected=ozState.masterConnected;const setMasterConnected=val=>{setOzState(prev=>({...prev,masterConnected:typeof val==='function'?val(prev.masterConnected):val}));};// ── VST FX masterbus (native_bridge --render-fx, áp dụng khi Export/Offline render) ── const masterVstChain=ozState.vstFxChain||[];// Embedded VST GUI state (PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 2) const[masterGuiOpen,setMasterGuiOpenState]=React.useState(_persistMasterGuiOpen);// { path, url } | null -const setMasterGuiOpen=v=>{_persistMasterGuiOpen=v;setMasterGuiOpenState(v);};const[activeMasterVstPath,setActiveMasterVstPath]=React.useState(null);const masterGuiOpenRef=React.useRef(null);masterGuiOpenRef.current=masterGuiOpen;const closeMasterGui=()=>{const g=masterGuiOpenRef.current;if(g){try{fetch(g.url+'/hide',{method:'POST'}).catch(()=>{});}catch(e){}}setMasterGuiOpen(null);setActiveMasterVstPath(null);setOzState(prev=>{const n={...(prev||{})};delete n.gui_open_path;delete n.gui_open_name;return n;});};React.useEffect(()=>()=>{const g=masterGuiOpenRef.current;if(g){try{fetch(g.url+'/hide',{method:'POST'}).catch(()=>{});}catch(e){}}},[]);// Capture GUI knob tweaks while the embedded GUI is open (bridge /preset). +const setMasterGuiOpen=v=>{_persistMasterGuiOpen=v;setMasterGuiOpenState(v);};const[activeMasterVstPath,setActiveMasterVstPath]=React.useState(null);const masterGuiOpenRef=React.useRef(null);masterGuiOpenRef.current=masterGuiOpen;const closeMasterGui=()=>{const g=masterGuiOpenRef.current;if(g){try{fetch(g.url+'/close',{method:'POST'}).catch(()=>{});}catch(e){}}setMasterGuiOpen(null);setActiveMasterVstPath(null);setOzState(prev=>{const n={...(prev||{})};delete n.gui_open_path;delete n.gui_open_name;return n;});};React.useEffect(()=>()=>{const g=masterGuiOpenRef.current;if(g){try{fetch(g.url+'/hide',{method:'POST'}).catch(()=>{});}catch(e){}}},[]);// Capture GUI knob tweaks while the embedded GUI is open (bridge /preset). React.useEffect(()=>{if(!masterGuiOpen||!masterGuiOpen.url)return;const iv=setInterval(()=>{fetch(masterGuiOpen.url+'/preset').then(r=>r.json()).then(d=>{if(d&&d.preset_b64)_capturedPresetByPath[masterGuiOpen.path]=d.preset_b64;}).catch(()=>{});},1500);return()=>clearInterval(iv);// eslint-disable-next-line react-hooks/exhaustive-deps },[masterGuiOpen]);// Reopen the saved GUI when the panel is opened after a project restore. React.useEffect(()=>{if(!isOpen||!ozState||!ozState.gui_open_path||masterGuiOpen)return;const s=(ozState.vstFxChain||[]).find(x=>x.path===ozState.gui_open_path);if(!s||!window.SonicAPI||!window.SonicAPI.openFxGui)return;window.SonicAPI.openFxGui({path:s.path,name:ozState.gui_open_name||s.name||s.path,embed:true}).then(r=>{if(r&&r.embed_url)setMasterGuiOpen({path:s.path,url:r.embed_url});}).catch(()=>{});// eslint-disable-next-line react-hooks/exhaustive-deps -},[isOpen,ozState&&ozState.gui_open_path]);const setMasterVstChain=next=>setOzState(prev=>({...prev,vstFxChain:next}));// T2.4: push the master VST FX chain to the realtime bridge (live master -// mix). v1: master chain only — per-track vstChain stays offline (defer). -React.useEffect(()=>{if(!window.NativeBridgeService||!window.NativeBridgeService.setFxChain)return;const chain=(ozState.vstFxChain||[]).map(s=>({path:s.path||'',bypass:!!s.bypass,preset_b64:s.preset_b64||''}));window.NativeBridgeService.setFxChain(JSON.stringify(chain));},[ozState.vstFxChain]);const toggleMasterVst=idx=>setMasterVstChain(masterVstChain.map((s,i)=>i===idx?{...s,bypass:!s.bypass}:s));const removeMasterVst=idx=>{const rem=masterVstChain[idx];if(rem)delete _capturedPresetByPath[rem.path];setMasterVstChain(masterVstChain.filter((_,i)=>i!==idx));};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);};const openMasterVstGui=s=>{if(!s||!s.path){window.showToast&&window.showToast('Chọn VST FX trước','warning');return;}if(masterGuiOpen)closeMasterGui();if(s.path)setActiveMasterVstPath(s.path);if(!window.SonicAPI||!window.SonicAPI.openFxGui){window.showToast&&window.showToast('Máy này không hỗ trợ mở GUI VST','warning');return;}window.SonicAPI.openFxGui({path:s.path,name:s.name||s.path,embed:true}).then(r=>{if(r&&r.embed_url){setMasterGuiOpen({path:s.path,url:r.embed_url,name:s.name||s.path});setOzState(prev=>({...(prev||{}),gui_open_path:s.path,gui_open_name:s.name||s.path}));}else if(r&&r.already_running)window.showToast&&window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)','info');else window.showToast&&window.showToast('Không mở được GUI embed','error');}).catch(err=>window.showToast&&window.showToast('Lỗi mở GUI: '+(err.message||err),'error'));};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}]);openMasterVstGui({path:fx.path,name:fx.name||fx.id});};const changeMasterVstPlugin=(idx,path)=>{const fx=(masterFxList||[]).find(f=>f.path===path);delete _capturedPresetByPath[path];setMasterVstChain(masterVstChain.map((s,i)=>i===idx?{...s,path,name:fx?fx.name||fx.id:s.name,preset_b64:''}:s));};const uploadMasterVstPreset=idx=>{const inp=document.createElement('input');inp.type='file';inp.accept='.vstpreset,.fxp,.fxb,.dspreset';inp.onchange=()=>{const f=inp.files&&inp.files[0];if(!f)return;const reader=new FileReader();reader.onload=()=>{const b64=String(reader.result||'').split(',')[1]||'';setMasterVstChain(masterVstChain.map((s,i)=>i===idx?{...s,preset_b64:b64}:s));window.showToast&&window.showToast('Đã gắn preset .vstpreset vào slot '+(idx+1),'success');};reader.readAsDataURL(f);};inp.click();};const previewMasterFx=async()=>{try{if(previewAudioRef.current){previewAudioRef.current.pause();previewAudioRef.current=null;}if(!window.__renderProjectPreview){window.showToast&&window.showToast('Không có hàm render preview','warning');return;}window.showToast&&window.showToast('Đang render offline (bridge)...','info');const res=await window.__renderProjectPreview();const au=new Audio((window.API_BASE_URL||'')+res.url);previewAudioRef.current=au;au.onended=()=>{previewAudioRef.current=null;};au.play().then(()=>window.showToast&&window.showToast('Preview offline — âm thật = âm export (VST FX)','success')).catch(()=>{});}catch(e){/* toast ở __renderProjectPreview */}};const audioRef=React.useRef({source:null});const eqCanvasRef=React.useRef(null);const imagerCanvasRef=React.useRef(null);const inMeterCanvasRef=React.useRef(null);const outMeterCanvasRef=React.useRef(null);const animFrameRef=React.useRef(null);const lufsAccRef=React.useRef([]);const knobsInitializedRef=React.useRef(false);// VST FX masterbus (native_bridge --render-fx) — list VST3 effect plugins + preview +},[isOpen,ozState&&ozState.gui_open_path]);const setMasterVstChain=next=>setOzState(prev=>({...prev,vstFxChain:next}));// LIVE master FX xử lý bởi fxRtStart() (SHM loop qua fx_vst_bridge +// --realtime-fx) — KHÔNG push chain xuống native bridge (setFxChain): +// nếu push, MIDI/VSTi instrument bị xử lý 2 lần (native bridge + SHM loop) +// → over-process, bể/crackle. setFxChain vẫn dùng khi Export/Offline render. +const toggleMasterVst=idx=>setMasterVstChain(masterVstChain.map((s,i)=>i===idx?{...s,bypass:!s.bypass}:s));const removeMasterVst=idx=>{const rem=masterVstChain[idx];if(rem)delete _capturedPresetByPath[rem.path];setMasterVstChain(masterVstChain.filter((_,i)=>i!==idx));};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);};const openMasterVstGui=s=>{if(!s||!s.path){window.showToast&&window.showToast('Chọn VST FX trước','warning');return;}if(masterGuiOpen)closeMasterGui();if(s.path)setActiveMasterVstPath(s.path);if(!window.SonicAPI||!window.SonicAPI.openFxGui){window.showToast&&window.showToast('Máy này không hỗ trợ mở GUI VST','warning');return;}window.SonicAPI.openFxGui({path:s.path,name:s.name||s.path,embed:true}).then(r=>{if(r&&r.embed_url){setMasterGuiOpen({path:s.path,url:r.embed_url,name:s.name||s.path});setOzState(prev=>({...(prev||{}),gui_open_path:s.path,gui_open_name:s.name||s.path}));}else if(r&&r.already_running&&r.embed_url){setMasterGuiOpen({path:s.path,url:r.embed_url,name:s.name||s.path});setOzState(prev=>({...(prev||{}),gui_open_path:s.path,gui_open_name:s.name||s.path}));}else if(r&&r.already_running)window.showToast&&window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)','info');else window.showToast&&window.showToast('Không mở được GUI embed','error');}).catch(err=>window.showToast&&window.showToast('Lỗi mở GUI: '+(err.message||err),'error'));};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}]);openMasterVstGui({path:fx.path,name:fx.name||fx.id});};const changeMasterVstPlugin=(idx,path)=>{const fx=(masterFxList||[]).find(f=>f.path===path);delete _capturedPresetByPath[path];setMasterVstChain(masterVstChain.map((s,i)=>i===idx?{...s,path,name:fx?fx.name||fx.id:s.name,preset_b64:''}:s));};const uploadMasterVstPreset=idx=>{const inp=document.createElement('input');inp.type='file';inp.accept='.vstpreset,.fxp,.fxb,.dspreset';inp.onchange=()=>{const f=inp.files&&inp.files[0];if(!f)return;const reader=new FileReader();reader.onload=()=>{const b64=String(reader.result||'').split(',')[1]||'';setMasterVstChain(masterVstChain.map((s,i)=>i===idx?{...s,preset_b64:b64}:s));window.showToast&&window.showToast('Đã gắn preset .vstpreset vào slot '+(idx+1),'success');};reader.readAsDataURL(f);};inp.click();};const previewMasterFx=async()=>{try{if(previewAudioRef.current){previewAudioRef.current.pause();previewAudioRef.current=null;}if(!window.__renderProjectPreview){window.showToast&&window.showToast('Không có hàm render preview','warning');return;}window.showToast&&window.showToast('Đang render offline (bridge)...','info');const res=await window.__renderProjectPreview();const au=new Audio((window.API_BASE_URL||'')+res.url);previewAudioRef.current=au;au.onended=()=>{previewAudioRef.current=null;};au.play().then(()=>window.showToast&&window.showToast('Preview offline — âm thật = âm export (VST FX)','success')).catch(()=>{});}catch(e){/* toast ở __renderProjectPreview */}};const audioRef=React.useRef({source:null});const eqCanvasRef=React.useRef(null);const imagerCanvasRef=React.useRef(null);const inMeterCanvasRef=React.useRef(null);const outMeterCanvasRef=React.useRef(null);const animFrameRef=React.useRef(null);const lufsAccRef=React.useRef([]);const knobsInitializedRef=React.useRef(false);// VST FX masterbus (native_bridge --render-fx) — list VST3 effect plugins + preview const[masterFxList,setMasterFxList]=React.useState([]);const[masterFxLoading,setMasterFxLoading]=React.useState(false);React.useEffect(()=>{if(window.SonicAPI&&window.SonicAPI.listFx){setMasterFxLoading(true);window.SonicAPI.listFx().then(d=>setMasterFxList(d&&d.plugins||[])).catch(()=>setMasterFxList([])).finally(()=>setMasterFxLoading(false));}},[isOpen]);const previewAudioRef=React.useRef(null);const[addMasterVstSel,setAddMasterVstSel]=React.useState('');const[masterVstPickerOpen,setMasterVstPickerOpen]=React.useState(false);// Wave Observer Refs & States const woCanvasRef=React.useRef(null);const woLeftHistoryRef=React.useRef(new Float32Array(400).fill(0));const woRightHistoryRef=React.useRef(new Float32Array(400).fill(0));const woLeftMeterRef=React.useRef(null);const woRightMeterRef=React.useRef(null);const[woPaused,setWoPaused]=React.useState(false);const[woChannel,setWoChannel]=React.useState('stereo');const[woMode,setWoMode]=React.useState('waveform');const[woDuration,setWoDuration]=React.useState(2.0);const[woZoom,setWoZoom]=React.useState(0.0);const woPausedRef=React.useRef(woPaused);woPausedRef.current=woPaused;const woChannelRef=React.useRef(woChannel);woChannelRef.current=woChannel;const woModeRef=React.useRef(woMode);woModeRef.current=woMode;const woDurationRef=React.useRef(woDuration);woDurationRef.current=woDuration;const woZoomRef=React.useRef(woZoom);woZoomRef.current=woZoom;const ozStateRef=React.useRef(ozState);ozStateRef.current=ozState;function startAudioDemo(){getAudioContext();const ctx=audioCtx;if(ctx.state==='suspended')ctx.resume();stopAudioDemo();const sampleRate=ctx.sampleRate;const bufferSize=sampleRate*4;const buffer=ctx.createBuffer(2,bufferSize,sampleRate);const left=buffer.getChannelData(0);const right=buffer.getChannelData(1);for(let i=0;i{if(!isOpen)return;getAudioContext();function resizeAll(){const resizeCanvas=ref=>{const el=ref.current;if(el){el.width=el.clientWidth;el.height=el.clientHeight;}};resizeCanvas(eqCanvasRef);resizeCanvas(imagerCanvasRef);resizeCanvas(inMeterCanvasRef);resizeCanvas(outMeterCanvasRef);resizeCanvas(woCanvasRef);}resizeAll();window.addEventListener('resize',resizeAll);const fftData=new Uint8Array(1024);function getPeakLevel(analyser){if(!analyser)return 0;const bufferLength=analyser.fftSize;const dataArray=new Float32Array(bufferLength);analyser.getFloatTimeDomainData(dataArray);let maxVal=0;for(let i=0;imaxVal){maxVal=val;}}return maxVal;}function renderFrame(){animFrameRef.current=requestAnimationFrame(renderFrame);const s=ozStateRef.current;// EQ Spectrum const eqCanvas=eqCanvasRef.current;if(eqCanvas){const w=eqCanvas.width,h=eqCanvas.height;const eqCtx=eqCanvas.getContext('2d');eqCtx.clearRect(0,0,w,h);eqCtx.strokeStyle='rgba(51, 65, 85, 0.3)';eqCtx.lineWidth=1;eqCtx.font='9px JetBrains Mono';eqCtx.fillStyle='#475569';const freqs=[20,50,100,200,500,1000,2000,5000,10000,20000];freqs.forEach(f=>{const x=Math.log10(f/20)/Math.log10(20000/20)*w;eqCtx.beginPath();eqCtx.moveTo(x,0);eqCtx.lineTo(x,h);eqCtx.stroke();if(f>=1000)eqCtx.fillText(`${f/1000}k`,x+3,h-6);else eqCtx.fillText(`${f}`,x+3,h-6);});if(masterBus&&masterBus.outputAnalyser){masterBus.outputAnalyser.getByteFrequencyData(fftData);eqCtx.fillStyle='rgba(56, 189, 248, 0.15)';const barWidth=w/128;for(let i=0;i<128;i++){const val=fftData[i*4]/255;eqCtx.fillRect(i*barWidth,h-val*h,barWidth-1,val*h);}}eqCtx.strokeStyle='#38bdf8';eqCtx.lineWidth=2.5;eqCtx.beginPath();for(let x=0;x - +