V45.45: fix GUI embed VST den tren box (Nectar/iZotope) - first-paint kick real click + BitBlt black-frame fallback + on-screen parking MAC DINH (SF_FXGUI_EMBED_ONSCREEN=0 de tat) + plugins.py log TAT CA stdout bridge (khong nuot dong truoc SF_FXGUI_PORT=) + auto-open GUI khi add VST FX vao Mastering slot. Rebuild daw_vst_bridge+fx_vst_bridge+installer NSIS.
This commit is contained in:
+24
-20
@@ -1,4 +1,4 @@
|
||||
import os, sys, uuid, json, tempfile, subprocess, time as _time, threading, asyncio, logging
|
||||
import os, sys, uuid, json, tempfile, subprocess, time as _time, threading, asyncio, logging, queue
|
||||
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Query, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
@@ -744,15 +744,31 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
|
||||
close_fds=os.name != "nt",
|
||||
env=dict(os.environ, SF_PARENT_PID=str(os.getpid())),
|
||||
creationflags=_gui_creationflags)
|
||||
# Bridge prints SF_FXGUI_PORT=<port> after binding its HTTP server.
|
||||
# Drain stdout NGAY TU DAU (daemon thread) và log TẤT CẢ các dòng
|
||||
# bridge in — gồm cả các dòng in TRƯỚC SF_FXGUI_PORT= ("[FxGui]
|
||||
# env ..." của RenderFxJob.cpp) mà cách đọc-tới-port cũ nuốt mất,
|
||||
# làm log engine không chứng minh được binary/env nào đang chạy.
|
||||
# Port được chuyển về qua queue (thread này không đọc stdout).
|
||||
port_q = queue.Queue(maxsize=1)
|
||||
|
||||
def _drain_fx_gui_stdout(pp):
|
||||
try:
|
||||
for ln in pp.stdout:
|
||||
ln = (ln or "").strip()
|
||||
if ln:
|
||||
logger.info("[fx-gui] %s", ln)
|
||||
if ln.startswith("SF_FXGUI_PORT="):
|
||||
try:
|
||||
port_q.put_nowait(int(ln.split("=", 1)[1]))
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
threading.Thread(target=_drain_fx_gui_stdout, args=(proc,), daemon=True).start()
|
||||
embed_url = None
|
||||
try:
|
||||
for line in proc.stdout:
|
||||
line = (line or "").strip()
|
||||
if line.startswith("SF_FXGUI_PORT="):
|
||||
port = int(line.split("=", 1)[1])
|
||||
embed_url = f"http://127.0.0.1:{port}"
|
||||
break
|
||||
port = port_q.get(timeout=60)
|
||||
embed_url = f"http://127.0.0.1:{port}"
|
||||
except Exception:
|
||||
pass
|
||||
if not embed_url:
|
||||
@@ -762,18 +778,6 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
|
||||
except Exception:
|
||||
proc.kill()
|
||||
raise HTTPException(status_code=500, detail="Bridge không báo port fx-gui (mở GUI thất bại).")
|
||||
# Keep draining stdout in a daemon thread: the bridge logs to
|
||||
# stdout/stderr while running; if the pipe fills (64KB) the bridge
|
||||
# blocks on any later write -> GUI freezes/breaks.
|
||||
def _drain_fx_gui_stdout(pp):
|
||||
try:
|
||||
for ln in pp.stdout:
|
||||
ln = (ln or "").strip()
|
||||
if ln:
|
||||
logger.info("[fx-gui] %s", ln)
|
||||
except Exception:
|
||||
pass
|
||||
threading.Thread(target=_drain_fx_gui_stdout, args=(proc,), daemon=True).start()
|
||||
proc._sf_plugin_path = plugin_path
|
||||
proc._sf_embed_url = embed_url
|
||||
_register_fx_gui_process(proc)
|
||||
|
||||
@@ -14929,6 +14929,9 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
|
||||
vstFxChain: [...(prev.vstFxChain || []), { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]
|
||||
}));
|
||||
setAddModuleOpen(false);
|
||||
// Tự mở GUI embed ngay khi add VST FX vào slot — user không cần bấm vào
|
||||
// slot trong chain để hiển thị nữa (yêu cầu 2026-09-03).
|
||||
openMasterVstGui({ path: fx.path, name: fx.name || fx.id });
|
||||
};
|
||||
const updateChainEntryParams = (modId, patch) => {
|
||||
setOzState(prev => ({
|
||||
|
||||
@@ -348,7 +348,7 @@ else if(fxRt.chainKey!==ck){fxRtSetChain(chain);fxRt.chainKey=ck;return;}else re
|
||||
// node → bypass vô hiệu. Mỗi fxRtStop() bump startToken → mọi checkpoint
|
||||
// dưới đây thấy mismatch → hủy session vừa tạo, không nối node.
|
||||
const token=++fxRt.startToken;fxRt.starting=true;const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet||!masterBus||!masterBus.output||!masterBus.analyser||!masterBus.masterFxGain||!masterBus.masterFxDryGain)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(window.currentMasteringSettings||{}).vstBypass}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT] start fail:',e);showToast('Không khởi động VST FX realtime: '+(e.message||e),'warning');fxRt.starting=false;return;}if(token!==fxRt.startToken){// Bypass/stop giữa lúc engine đang load chain — hủy session vừa tạo.
|
||||
console.warn('[FX RT] start cancelled mid-flight — session stopped');try{window.SonicAPI.fxRealtimeStop({session_id:st.session_id}).catch(()=>{});}catch(e2){}fxRt.starting=false;return;}fxRt.sessionId=st.session_id;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202609022050`);if(token!==fxRt.startToken){fxRt.starting=false;return;}fxRt.node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms: Chromium thu ::1 truoc (Windows khong
|
||||
console.warn('[FX RT] start cancelled mid-flight — session stopped');try{window.SonicAPI.fxRealtimeStop({session_id:st.session_id}).catch(()=>{});}catch(e2){}fxRt.starting=false;return;}fxRt.sessionId=st.session_id;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202609030646`);if(token!==fxRt.startToken){fxRt.starting=false;return;}fxRt.node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms: Chromium thu ::1 truoc (Windows khong
|
||||
// co listener IPv6 -> SYN hang ~2s) roi moi fallback 127.0.0.1. Dung
|
||||
// thang 127.0.0.1 (loopback) -> WS open ~5ms. Server chap nhan loopback.
|
||||
const wsHost=u.hostname==='localhost'||u.hostname==='::1'?'127.0.0.1'+(u.port?':'+u.port:''):u.host;const tok=localStorage.getItem('sonic_token')||'';const wsUrl=`${wsProto}${wsHost}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`;const ws=new WebSocket(wsUrl);fxRt.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{if(token!==fxRt.startToken){try{ws.close();}catch(e){}fxRt.starting=false;return;}fxRt.active=true;fxRt.flushing=false;fxRt.starting=false;fxRt.inQueue=[];// bỏ audio stale sản xuất trước khi WS open (~4s load VST) — hết lag khởi đầu
|
||||
@@ -1115,7 +1115,9 @@ if(woChannelRef.current==='stereo'||woChannelRef.current==='left'){woCtx.strokeS
|
||||
if(woChannelRef.current==='stereo'||woChannelRef.current==='right'){woCtx.strokeStyle='#0d9488';woCtx.lineWidth=1.2;woCtx.beginPath();for(let i=0;i<rHistory.length;i++){const x=i/(rHistory.length-1)*w;const val=rHistory[i]*zoomGain;const y=centerY-val*centerY;if(i===0)woCtx.moveTo(x,y);else woCtx.lineTo(x,y);}woCtx.stroke();if(woModeRef.current==='envelope'){woCtx.beginPath();for(let i=0;i<rHistory.length;i++){const x=i/(rHistory.length-1)*w;const val=rHistory[i]*zoomGain;const y=centerY+val*centerY;if(i===0)woCtx.moveTo(x,y);else woCtx.lineTo(x,y);}woCtx.stroke();}}}}renderFrame();return()=>{window.removeEventListener('resize',resizeAll);if(animFrameRef.current)cancelAnimationFrame(animFrameRef.current);};},[isOpen]);React.useEffect(()=>{if(!isOpen){stopAudioDemo();knobsInitializedRef.current=false;}else{setTimeout(()=>{try{window.lucide.createIcons();}catch(e){}},100);}},[isOpen]);// Hooks MUST be declared before the early return below — React requires a
|
||||
// stable hook count across renders (error #310 otherwise).
|
||||
const dragChainIndexRef=React.useRef(null);const[addModuleOpen,setAddModuleOpen]=React.useState(false);if(!isOpen)return null;const switchModule=name=>setOzState(prev=>({...prev,activeModule:name}));// ── Mastering expand: dynamic module chain (mastering_expand.md §II.3) ──
|
||||
const MODULE_META={eq:{name:'Dynamic EQ',sub:'4-Band Peak',icon:'activity',color:'#22d3ee'},eqpro:{name:'Parametric / Graphic EQ PRO',sub:'Pro-Q style · 8 bands · interactive',icon:'chart-area',color:'#2dd4bf'},imager:{name:'Imager',sub:'4-Band Width',icon:'radio',color:'#a855f7'},maximizer:{name:'Maximizer',sub:'IRC IV True Peak',icon:'gauge',color:'#34d399'},compressor:{name:'Bus Compressor',sub:'Glue & Punch',icon:'compress',color:'#fbbf24'},limiter:{name:'Brickwall Limiter',sub:'True-Peak 20:1',icon:'shield-half',color:'#f43f5e'},multiband:{name:'Multiband Compressor',sub:'3-Band LR4',icon:'layers',color:'#a3e635'},deesser:{name:'De-Esser',sub:'Sibilance tamer',icon:'mic-2',color:'#fb923c'},exciter:{name:'Harmonic Exciter',sub:'Saturation & Air',icon:'wand-2',color:'#c084fc'},rebalance:{name:'Master Rebalance',sub:'M/S Balance',icon:'sliders-horizontal',color:'#38bdf8'},vst3:{name:'VST FX',sub:'VST3 plugin',icon:'plug',color:'#2dd4bf'}};const chainFlag=type=>type==='eq'?'eqActive':type==='eqpro'?'eqproActive':type==='imager'?'imagerActive':type==='maximizer'?'maximizerActive':type==='compressor'?'compActive':type==='limiter'?'limActive':type==='exciter'?'excActive':type==='vst3'?'vstFxActive':type==='multiband'?'mbActive':type==='deesser'?'deessActive':'rebalActive';const chainActive=type=>!!ozState[chainFlag(type)];const mb=(ozState.chain||[]).find(m=>m.type==='multiband');const de=(ozState.chain||[]).find(m=>m.type==='deesser');const toggleChainModule=modId=>{setOzState(prev=>{const chain=prev.chain.map(m=>{if(m.id!==modId)return m;const next=!m.active;return{...m,active:next};});const flags={};chain.forEach(m=>{flags[chainFlag(m.type)]=!!m.active;});const target=chain.find(m=>m.id===modId);let vstFxChain=prev.vstFxChain||[];if(target&&target.type==='vst3'){const vi=vstFxChain.findIndex(s=>s.path===target.path);if(vi>=0)vstFxChain=vstFxChain.map((s,si)=>si===vi?{...s,bypass:!target.active}:s);}return{...prev,chain,...flags,vstFxChain};});};const removeChainModule=modId=>{const removedMod=(ozState.chain||[]).find(m=>m.id===modId);if(removedMod&&removedMod.type==='vst3'&&masterGuiOpen&&masterGuiOpen.path===removedMod.path)closeMasterGui();setOzState(prev=>{const removed=prev.chain.find(m=>m.id===modId);const chain=prev.chain.filter(m=>m.id!==modId);const flags={};chain.forEach(m=>{flags[chainFlag(m.type)]=!!m.active;});const activeModule=prev.activeModule;let vstFxChain=prev.vstFxChain||[];if(removed&&removed.type==='vst3'){const vi=vstFxChain.findIndex(s=>s.path===removed.path);if(vi>=0)vstFxChain=[...vstFxChain.slice(0,vi),...vstFxChain.slice(vi+1)];}return{...prev,chain,...flags,vstFxChain,activeModule:chain.length?chain.some(m=>m.type===activeModule)?activeModule:chain[chain.length-1].type:null};});};const reorderChain=(fromIdx,toIdx)=>{if(fromIdx===toIdx)return;setOzState(prev=>{const chain=[...prev.chain];const moved=chain.splice(fromIdx,1)[0];chain.splice(toIdx,0,moved);const vstFxChain=chain.filter(m=>m&&m.type==='vst3').map(s=>({type:'vst3',path:s.path||'',name:s.name||'',preset_b64:s.preset_b64||'',bypass:!!s.bypass}));return{...prev,chain,vstFxChain};});};const addModuleToChain=type=>{const meta=MODULE_META[type];const id='mod_'+type+'_'+Date.now();const entry={id,type,name:meta.name,active:true};if(type==='eqpro')entry.params={amount:100,bands:JSON.parse(JSON.stringify(EQPRO_DEFAULT_BANDS))};if(type==='multiband')entry.params={lf_cross:200,hf_cross:4000,low_thr:-24,low_ratio:3,low_makeup:0,mid_thr:-24,mid_ratio:3,mid_makeup:0,high_thr:-24,high_ratio:3,high_makeup:0};if(type==='deesser')entry.params={threshold:-30,ratio:40,freq:6000,makeup:0};setOzState(prev=>({...prev,chain:[...(prev.chain||[]),entry],[chainFlag(type)]:true,activeModule:type}));setAddModuleOpen(false);};const addVstToChain=path=>{const fx=(masterFxList||[]).find(f=>f.path===path);if(!fx)return;const id='mod_vst_'+Date.now();const entry={id,type:'vst3',path:fx.path,name:fx.name||fx.id,preset_b64:'',bypass:false,active:true};setOzState(prev=>({...prev,chain:[...(prev.chain||[]),entry],vstFxChain:[...(prev.vstFxChain||[]),{type:'vst3',path:fx.path,name:fx.name||fx.id,preset_b64:'',bypass:false}]}));setAddModuleOpen(false);};const updateChainEntryParams=(modId,patch)=>{setOzState(prev=>({...prev,chain:(prev.chain||[]).map(m=>m.id===modId?{...m,params:{...(m.params||{}),...patch}}:m)}));};// ── EQ Preset Library (mastering_expand.md §II.1) ──
|
||||
const MODULE_META={eq:{name:'Dynamic EQ',sub:'4-Band Peak',icon:'activity',color:'#22d3ee'},eqpro:{name:'Parametric / Graphic EQ PRO',sub:'Pro-Q style · 8 bands · interactive',icon:'chart-area',color:'#2dd4bf'},imager:{name:'Imager',sub:'4-Band Width',icon:'radio',color:'#a855f7'},maximizer:{name:'Maximizer',sub:'IRC IV True Peak',icon:'gauge',color:'#34d399'},compressor:{name:'Bus Compressor',sub:'Glue & Punch',icon:'compress',color:'#fbbf24'},limiter:{name:'Brickwall Limiter',sub:'True-Peak 20:1',icon:'shield-half',color:'#f43f5e'},multiband:{name:'Multiband Compressor',sub:'3-Band LR4',icon:'layers',color:'#a3e635'},deesser:{name:'De-Esser',sub:'Sibilance tamer',icon:'mic-2',color:'#fb923c'},exciter:{name:'Harmonic Exciter',sub:'Saturation & Air',icon:'wand-2',color:'#c084fc'},rebalance:{name:'Master Rebalance',sub:'M/S Balance',icon:'sliders-horizontal',color:'#38bdf8'},vst3:{name:'VST FX',sub:'VST3 plugin',icon:'plug',color:'#2dd4bf'}};const chainFlag=type=>type==='eq'?'eqActive':type==='eqpro'?'eqproActive':type==='imager'?'imagerActive':type==='maximizer'?'maximizerActive':type==='compressor'?'compActive':type==='limiter'?'limActive':type==='exciter'?'excActive':type==='vst3'?'vstFxActive':type==='multiband'?'mbActive':type==='deesser'?'deessActive':'rebalActive';const chainActive=type=>!!ozState[chainFlag(type)];const mb=(ozState.chain||[]).find(m=>m.type==='multiband');const de=(ozState.chain||[]).find(m=>m.type==='deesser');const toggleChainModule=modId=>{setOzState(prev=>{const chain=prev.chain.map(m=>{if(m.id!==modId)return m;const next=!m.active;return{...m,active:next};});const flags={};chain.forEach(m=>{flags[chainFlag(m.type)]=!!m.active;});const target=chain.find(m=>m.id===modId);let vstFxChain=prev.vstFxChain||[];if(target&&target.type==='vst3'){const vi=vstFxChain.findIndex(s=>s.path===target.path);if(vi>=0)vstFxChain=vstFxChain.map((s,si)=>si===vi?{...s,bypass:!target.active}:s);}return{...prev,chain,...flags,vstFxChain};});};const removeChainModule=modId=>{const removedMod=(ozState.chain||[]).find(m=>m.id===modId);if(removedMod&&removedMod.type==='vst3'&&masterGuiOpen&&masterGuiOpen.path===removedMod.path)closeMasterGui();setOzState(prev=>{const removed=prev.chain.find(m=>m.id===modId);const chain=prev.chain.filter(m=>m.id!==modId);const flags={};chain.forEach(m=>{flags[chainFlag(m.type)]=!!m.active;});const activeModule=prev.activeModule;let vstFxChain=prev.vstFxChain||[];if(removed&&removed.type==='vst3'){const vi=vstFxChain.findIndex(s=>s.path===removed.path);if(vi>=0)vstFxChain=[...vstFxChain.slice(0,vi),...vstFxChain.slice(vi+1)];}return{...prev,chain,...flags,vstFxChain,activeModule:chain.length?chain.some(m=>m.type===activeModule)?activeModule:chain[chain.length-1].type:null};});};const reorderChain=(fromIdx,toIdx)=>{if(fromIdx===toIdx)return;setOzState(prev=>{const chain=[...prev.chain];const moved=chain.splice(fromIdx,1)[0];chain.splice(toIdx,0,moved);const vstFxChain=chain.filter(m=>m&&m.type==='vst3').map(s=>({type:'vst3',path:s.path||'',name:s.name||'',preset_b64:s.preset_b64||'',bypass:!!s.bypass}));return{...prev,chain,vstFxChain};});};const addModuleToChain=type=>{const meta=MODULE_META[type];const id='mod_'+type+'_'+Date.now();const entry={id,type,name:meta.name,active:true};if(type==='eqpro')entry.params={amount:100,bands:JSON.parse(JSON.stringify(EQPRO_DEFAULT_BANDS))};if(type==='multiband')entry.params={lf_cross:200,hf_cross:4000,low_thr:-24,low_ratio:3,low_makeup:0,mid_thr:-24,mid_ratio:3,mid_makeup:0,high_thr:-24,high_ratio:3,high_makeup:0};if(type==='deesser')entry.params={threshold:-30,ratio:40,freq:6000,makeup:0};setOzState(prev=>({...prev,chain:[...(prev.chain||[]),entry],[chainFlag(type)]:true,activeModule:type}));setAddModuleOpen(false);};const addVstToChain=path=>{const fx=(masterFxList||[]).find(f=>f.path===path);if(!fx)return;const id='mod_vst_'+Date.now();const entry={id,type:'vst3',path:fx.path,name:fx.name||fx.id,preset_b64:'',bypass:false,active:true};setOzState(prev=>({...prev,chain:[...(prev.chain||[]),entry],vstFxChain:[...(prev.vstFxChain||[]),{type:'vst3',path:fx.path,name:fx.name||fx.id,preset_b64:'',bypass:false}]}));setAddModuleOpen(false);// Tự mở GUI embed ngay khi add VST FX vào slot — user không cần bấm vào
|
||||
// slot trong chain để hiển thị nữa (yêu cầu 2026-09-03).
|
||||
openMasterVstGui({path:fx.path,name:fx.name||fx.id});};const updateChainEntryParams=(modId,patch)=>{setOzState(prev=>({...prev,chain:(prev.chain||[]).map(m=>m.id===modId?{...m,params:{...(m.params||{}),...patch}}:m)}));};// ── EQ Preset Library (mastering_expand.md §II.1) ──
|
||||
const EQ_PRESET_LIBRARY={flat:{name:'Flat / Reset',bands:[{id:1,type:'lowshelf',freq:100,gain:0.0,q:0.7},{id:2,type:'peaking',freq:800,gain:0.0,q:0.7},{id:3,type:'peaking',freq:3200,gain:0.0,q:1.2},{id:4,type:'highshelf',freq:10000,gain:0.0,q:0.7}]},vocal_clarity:{name:'Vocal Unmask & Clarity',bands:[{id:1,type:'lowshelf',freq:90,gain:-2.5,q:0.7},{id:2,type:'peaking',freq:500,gain:-1.8,q:1.0},{id:3,type:'peaking',freq:2800,gain:3.2,q:1.2},{id:4,type:'highshelf',freq:12000,gain:2.0,q:0.7}]},bass_punch:{name:'EDM Low-End Punch',bands:[{id:1,type:'lowshelf',freq:80,gain:4.0,q:0.8},{id:2,type:'peaking',freq:300,gain:-3.0,q:1.4},{id:3,type:'peaking',freq:4000,gain:1.5,q:1.0},{id:4,type:'highshelf',freq:10000,gain:1.0,q:0.7}]},warm_tape:{name:'Warm Vintage Analog',bands:[{id:1,type:'lowshelf',freq:120,gain:2.0,q:0.6},{id:2,type:'peaking',freq:1500,gain:1.0,q:0.5},{id:3,type:'peaking',freq:5000,gain:-2.0,q:1.0},{id:4,type:'highshelf',freq:8000,gain:-3.0,q:0.7}]}};const applyEQPreset=presetKey=>{const preset=EQ_PRESET_LIBRARY[presetKey];if(!preset)return;// Phase 4.5: EQ DSP chạy trong bridge — preset chỉ sync state; engine
|
||||
// nhận qua fxRtPushAllParams() ở applyMasteringSettings.
|
||||
// Sync UI knobs + canvas: [eqLowGain, eqMid1Gain, eqMid2Gain, eqHighGain]
|
||||
@@ -2006,7 +2008,7 @@ fxRtTrackReconcile(trackId,node,track);}catch(e){console.warn('rebuildTrackFxGra
|
||||
// Cần activeTrackNodesRef/isPlayingRef. Session giống master (fxRtStart):
|
||||
// fetch start → AudioWorkletNode 'sf-fx-realtime' → WS; WS open → splice
|
||||
// worklet vào chain hiện tại (đọc mods MỚI NHẤT — an toàn với rebuild).
|
||||
const fxRtTrackStart=async(trackId,track,path)=>{const key=trackId+':'+path;const chain=fxRtTrackChain(track);if(!chain.length)return;if(fxRtTracks[key]){if(!fxRtTracks[key].node||fxRtTracks[key].node.context!==getAudioContext())fxRtTrackStop(trackId,path,true);else return;}const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT track] start fail',trackId,path,e);return;}const entry={sessionId:st.session_id,ws:null,node:null,ready:false,path,spliced:null,chainKey:fxRtTrackChainKey(chain)+'|vb:'+(track&&track.vstFxBypass?'1':'0'),paramsBySlot:chain.map(s=>fxRtEntryParams(s,null)),inQueue:[],pumpTimer:null,lastInSent:0,flushing:false,flushTimer:null};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202609022050`);const node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});entry.node=node;const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms (Chromium thu ::1 truoc roi fallback) - dung 127.0.0.1
|
||||
const fxRtTrackStart=async(trackId,track,path)=>{const key=trackId+':'+path;const chain=fxRtTrackChain(track);if(!chain.length)return;if(fxRtTracks[key]){if(!fxRtTracks[key].node||fxRtTracks[key].node.context!==getAudioContext())fxRtTrackStop(trackId,path,true);else return;}const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT track] start fail',trackId,path,e);return;}const entry={sessionId:st.session_id,ws:null,node:null,ready:false,path,spliced:null,chainKey:fxRtTrackChainKey(chain)+'|vb:'+(track&&track.vstFxBypass?'1':'0'),paramsBySlot:chain.map(s=>fxRtEntryParams(s,null)),inQueue:[],pumpTimer:null,lastInSent:0,flushing:false,flushTimer:null};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202609030646`);const node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});entry.node=node;const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms (Chromium thu ::1 truoc roi fallback) - dung 127.0.0.1
|
||||
const wsHost=u.hostname==='localhost'||u.hostname==='::1'?'127.0.0.1'+(u.port?':'+u.port:''):u.host;const tok=localStorage.getItem('sonic_token')||'';const ws=new WebSocket(`${wsProto}${wsHost}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`);entry.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{entry.ready=true;entry.inQueue=[];// bỏ audio stale trước khi WS open — hết lag khởi đầu
|
||||
fxRtTrackSplice(trackId,path);try{(entry.paramsBySlot||[]).forEach((pl,slot)=>pl.forEach(p=>ws.send(JSON.stringify({cmd:'set_param',slot,key:p.key,value:p.value}))));}catch(e){}};ws.onmessage=ev=>{if(typeof ev.data==='string'){try{const d=JSON.parse(ev.data);if(d&&d.cmd==='flush_done'){entry.flushing=false;if(entry.flushTimer){clearTimeout(entry.flushTimer);entry.flushTimer=null;}if(entry.node)entry.node.port.postMessage({type:'flush_done'});}}catch(e){}return;}if(!(ev.data instanceof ArrayBuffer)||!entry.node)return;if(entry.flushing)return;// V24: output stale trong luc flush — drop
|
||||
const f=new Float32Array(ev.data);const half=f.length/2;const l=new Float32Array(half);const r=new Float32Array(half);for(let i=0;i<half;i++){l[i]=f[i*2];r[i]=f[i*2+1];}entry.node.port.postMessage({type:'out',l,r});};ws.onclose=()=>fxRtTrackStop(trackId,path,true);ws.onerror=()=>{try{ws.close();}catch(e){}};entry.node.port.onmessage=ev=>{const d=ev.data;if(!d||!entry.ws)return;if(d.type==='flush'){// V24: nhu master — worklet underrun-resume → flush engine ring
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/chordTheory.js?v=202608290945"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202609022050" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202609030646" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202608290945">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
@@ -172,6 +172,14 @@ void sendOptions(SOCKET c) {
|
||||
// frame, while the GDI BitBlt view is a stale first paint for GPU-rendered
|
||||
// editors (JUCE/FAST Compressor) and never updates. BitBlt stays as fallback.
|
||||
// Returns JPEG.
|
||||
// Mean of the raw BGRA frame (0..255) — black-frame guard below.
|
||||
static double frameMean(const unsigned char* px, int w, int h) {
|
||||
const size_t n = (size_t)w * h;
|
||||
unsigned long long s = 0;
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
s += px[i * 4 + 2] + px[i * 4 + 1] + px[i * 4 + 0];
|
||||
return (double)s / (3.0 * (double)n);
|
||||
}
|
||||
bool captureHwndToJpeg(HWND hwnd, std::vector<unsigned char>& out, int quality,
|
||||
float* outAvg = nullptr, bool* outOkB = nullptr,
|
||||
bool* outOkP = nullptr) {
|
||||
@@ -207,7 +215,18 @@ bool captureHwndToJpeg(HWND hwnd, std::vector<unsigned char>& out, int quality,
|
||||
// for GPU-rendered editors (JUCE/FAST Compressor) and never updates, which
|
||||
// made the panel look frozen. BitBlt stays only as a fallback.
|
||||
unsigned char* px = nullptr;
|
||||
if (okP) px = pxP.data();
|
||||
if (okP && okB) {
|
||||
// Black-frame guard: PrintWindow can return TRUE with an ALL-BLACK
|
||||
// frame while the editor has not painted yet (iZotope-class, e.g.
|
||||
// Nectar — those editors paint only after their window was activated;
|
||||
// the /show first-paint kick fixes that, but until it fires BitBlt is
|
||||
// the only source of real pixels). If PrintWindow is pure black and
|
||||
// the GDI view has content, send the GDI view instead of a black panel.
|
||||
const double mP = frameMean(pxP.data(), w, h);
|
||||
const double mB = frameMean(pxB.data(), w, h);
|
||||
if (mP < 6.0 && mB > 6.0) px = pxB.data();
|
||||
else px = pxP.data();
|
||||
} else if (okP) px = pxP.data();
|
||||
else if (okB) px = pxB.data();
|
||||
if (outOkB) *outOkB = okB;
|
||||
if (outOkP) *outOkP = okP;
|
||||
@@ -277,15 +296,26 @@ POINT g_savedCursor = {0, 0}; // cursor to restore after the dance
|
||||
// rect = legacy cursor-relative mode.
|
||||
RECT g_embedRect = {0, 0, 0, 0};
|
||||
bool g_anchored = false; // window anchored over panel (stays until /close)
|
||||
// iZotope-class editors (Nectar/Ozone) paint their surface ONLY after the
|
||||
// window was actually visible AND activated at least once; parked HWND_BOTTOM
|
||||
// under the DAW they never paint -> black forever. /show performs a one-time
|
||||
// kick (raise above the DAW + invalidate + real click), then demotes the
|
||||
// window back under the DAW — the painted surface survives the demote.
|
||||
// Reset on /hide so each reopen re-kicks.
|
||||
std::atomic<bool> g_paintKicked{false};
|
||||
|
||||
// SF_FXGUI_EMBED_ONSCREEN=1: park the host window ON a monitor at the embed
|
||||
// panel rect (HWND_BOTTOM, under the DAW -> user never sees it) instead of
|
||||
// off-screen at -20000. DWM does not redirect windows parked fully off the
|
||||
// desktop on some drivers (Intel UHD 630 + iZotope GL editor) -> PrintWindow
|
||||
// captures only black. On a monitor (even occluded by the DAW above) DWM
|
||||
// keeps a redirection surface -> /frame gets real pixels. Real-input dance
|
||||
// still lifts the window to HWND_TOP for actual clicks (g_anchored guard).
|
||||
static const bool g_embedOnscreen = std::getenv("SF_FXGUI_EMBED_ONSCREEN") != nullptr;
|
||||
// On-screen embed parking (DEFAULT since V45.45): park the host window ON a
|
||||
// monitor at the embed panel rect (HWND_BOTTOM, under the DAW -> user never
|
||||
// sees it) instead of off-screen at -20000. DWM does not redirect windows
|
||||
// parked fully off the desktop on some drivers (Intel UHD 630 + iZotope GL
|
||||
// editor) -> capture sees only black. On a monitor (even occluded by the DAW
|
||||
// above) DWM keeps a redirection surface -> /frame gets real pixels. Real-input
|
||||
// dance still lifts the window to HWND_TOP for actual clicks (g_anchored
|
||||
// guard). SF_FXGUI_EMBED_ONSCREEN=0 disables (env kept for debugging).
|
||||
static const bool g_embedOnscreen = [] {
|
||||
const char* v = std::getenv("SF_FXGUI_EMBED_ONSCREEN");
|
||||
return v == nullptr || v[0] != '0';
|
||||
}();
|
||||
// SF_FXGUI_DEBUG=1: rate-limited /frame capture ground truth ([FxGuiDbg] lines).
|
||||
static const bool g_fxGuiDbg = std::getenv("SF_FXGUI_DEBUG") != nullptr;
|
||||
|
||||
@@ -522,8 +552,8 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
|
||||
// to show that blank frame as a white flash on open.
|
||||
sendResponse(c, 503, "application/json", "{\"error\":\"loading\"}", 19);
|
||||
} else {
|
||||
// EMBED_ONSCREEN: keep the host window ON a monitor at the
|
||||
// embed panel rect (HWND_BOTTOM, under the DAW) so DWM
|
||||
// On-screen embed parking: keep the host window ON a monitor
|
||||
// at the embed panel rect (HWND_BOTTOM, under the DAW) so DWM
|
||||
// redirects the editor's GPU surface and capture sees real
|
||||
// pixels — the one-shot rect /show may have run pre-attach
|
||||
// (deduped), so reconcile here on every poll until it fits.
|
||||
@@ -609,6 +639,7 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
|
||||
g_anchored = false;
|
||||
g_dance = false;
|
||||
g_parkUntil = 0;
|
||||
g_paintKicked = false; // next /show re-kicks the first paint
|
||||
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
||||
} else if (req.path == "/show") {
|
||||
// Optional body {rect:{x,y,w,h}} (screen px) — anchored placement
|
||||
@@ -635,43 +666,74 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
|
||||
}
|
||||
}
|
||||
if (sroot) std::free(sroot);
|
||||
// Embedded mode: the native window NEVER comes on-screen — it stays
|
||||
// parked off-screen at -20000,-20000 (GDI capture + synthetic input
|
||||
// work regardless of position). The panel rect sizes the host
|
||||
// window + plugin view so the capture frame matches the embed panel
|
||||
// (layout bug); /hide hid the window, so re-show it off-screen so
|
||||
// /frame keeps capturing.
|
||||
if (rectOk) {
|
||||
// Embedded mode: park the host window ON a monitor at the embed
|
||||
// panel rect (HWND_BOTTOM, under the DAW -> user never sees it)
|
||||
// instead of off-screen at -20000. iZotope-class editors never
|
||||
// paint while fully off-screen, and DWM does not redirect windows
|
||||
// parked off the desktop on some drivers (Intel UHD 630) — on a
|
||||
// monitor (even occluded by the DAW above) capture sees real
|
||||
// pixels. The panel rect also sizes the host window + plugin view
|
||||
// so the capture frame matches the embed panel (layout bug);
|
||||
// /hide hid the window, so re-show it here.
|
||||
// Skip while the editor is still attaching (pump thread busy;
|
||||
// SetWindowPos from this worker would block until load/attach
|
||||
// finishes) and while anchored/dancing (real-input owns the
|
||||
// window position during clicks).
|
||||
if (g_embedOnscreen && rectOk && g_fxGuiEditorReady.load() &&
|
||||
!g_anchored && !g_dance) {
|
||||
const int cw = g_embedRect.right - g_embedRect.left;
|
||||
const int ch = g_embedRect.bottom - g_embedRect.top;
|
||||
RECT cur = {};
|
||||
GetWindowRect(hwnd, &cur);
|
||||
// Skip when already the right size: frontend re-POSTs /show
|
||||
const bool samePlace =
|
||||
cur.left == g_embedRect.left && cur.top == g_embedRect.top &&
|
||||
cur.right - cur.left == cw && cur.bottom - cur.top == ch;
|
||||
// Skip when already parked right: frontend re-POSTs /show
|
||||
// every ~500ms; re-laying-out the plugin view every tick is
|
||||
// wasteful (WM_FXGUI_RESIZE -> checkSizeConstraint/onSize).
|
||||
if (cur.right - cur.left != cw || cur.bottom - cur.top != ch)
|
||||
fxGuiRequestResize(hwnd, cw, ch);
|
||||
}
|
||||
// EMBED_ONSCREEN: park the window ON the monitor at the embed
|
||||
// panel rect, HWND_BOTTOM (below the DAW -> user never sees it)
|
||||
// so DWM redirects the editor's GPU surface and /frame can
|
||||
// capture it. Skip while the editor is still attaching (the
|
||||
// pump thread is busy; SetWindowPos from here would block this
|
||||
// worker until load/attach finishes) and while anchored/dancing
|
||||
// (real-input keeps the window on top over the panel for clicks).
|
||||
if (g_embedOnscreen && rectOk && !g_anchored && !g_dance &&
|
||||
g_fxGuiEditorReady.load()) {
|
||||
const int cw = g_embedRect.right - g_embedRect.left;
|
||||
const int ch = g_embedRect.bottom - g_embedRect.top;
|
||||
RECT cur = {};
|
||||
GetWindowRect(hwnd, &cur);
|
||||
if (!IsWindowVisible(hwnd) || cur.left != g_embedRect.left ||
|
||||
cur.top != g_embedRect.top || cur.right - cur.left != cw ||
|
||||
cur.bottom - cur.top != ch)
|
||||
if (!samePlace || !IsWindowVisible(hwnd))
|
||||
SetWindowPos(hwnd, HWND_BOTTOM, g_embedRect.left,
|
||||
g_embedRect.top, cw, ch,
|
||||
SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||
if (cur.right - cur.left != cw || cur.bottom - cur.top != ch)
|
||||
fxGuiRequestResize(hwnd, cw, ch);
|
||||
// First-paint kick (one-time per /show cycle, only after the
|
||||
// editor attached). iZotope-class editors (Nectar/Ozone) paint
|
||||
// ONLY when their window is actually visible AND activated:
|
||||
// parked HWND_BOTTOM under the DAW they never paint -> black
|
||||
// forever, and SetForegroundWindow from this background process
|
||||
// is blocked by the Windows foreground lock. So: raise the
|
||||
// window ABOVE the DAW at the panel rect, paint it there, then
|
||||
// demote back under the DAW — the painted surface survives the
|
||||
// demote (capture stays non-black at HWND_BOTTOM).
|
||||
if (!g_paintKicked) {
|
||||
g_paintKicked = true;
|
||||
SetWindowPos(hwnd, HWND_TOP, g_embedRect.left, g_embedRect.top,
|
||||
cw, ch, SWP_SHOWWINDOW | SWP_NOACTIVATE);
|
||||
ShowWindow(hwnd, SW_SHOW);
|
||||
InvalidateRect(hwnd, nullptr, TRUE);
|
||||
RedrawWindow(hwnd, nullptr, nullptr,
|
||||
RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN);
|
||||
// Foreground-lock bypass: a real synthesized click (system
|
||||
// input, not subject to the SetForegroundWindow lock)
|
||||
// activates the window the same way a user click would.
|
||||
Sleep(200);
|
||||
RECT wr = {};
|
||||
GetWindowRect(hwnd, &wr);
|
||||
const int cx = wr.left + (wr.right - wr.left) / 2;
|
||||
const int cy = wr.top + 8; // title-bar strip, avoid knobs
|
||||
SetCursorPos(cx, cy);
|
||||
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
|
||||
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
|
||||
Sleep(500); // give the editor a beat to paint while on top
|
||||
SetWindowPos(hwnd, HWND_BOTTOM, g_embedRect.left, g_embedRect.top,
|
||||
cw, ch, SWP_NOACTIVATE);
|
||||
std::cerr << "[FxGui] first-paint kick fg=click @" << cx
|
||||
<< "," << cy << std::endl;
|
||||
}
|
||||
} else if (!IsWindowVisible(hwnd)) {
|
||||
// No usable rect yet (pre-layout / still attaching): keep the
|
||||
// window somewhere harmless until the frontend supplies it.
|
||||
SetWindowPos(hwnd, HWND_NOTOPMOST, -20000, -20000, 0, 0,
|
||||
SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||
}
|
||||
|
||||
@@ -1910,7 +1910,10 @@ int run_fx_gui_server(const std::string& jobPath) {
|
||||
// crash fix (attach on a real monitor) and the black-capture fix (stay
|
||||
// on a monitor afterwards, parked under the DAW at the panel rect) go
|
||||
// together — setting only EMBED_ONSCREEN must not regress the crash.
|
||||
const bool embedOnscreen = std::getenv("SF_FXGUI_EMBED_ONSCREEN") != nullptr;
|
||||
// V45.45: on-screen embed parking la MAC DINH (DWM Intel UHD 630 khong
|
||||
// redirect window parked -20000). Env chi con de TAT: SF_FXGUI_EMBED_ONSCREEN=0.
|
||||
const char* eosEnv = std::getenv("SF_FXGUI_EMBED_ONSCREEN");
|
||||
const bool embedOnscreen = eosEnv == nullptr || eosEnv[0] != '0';
|
||||
const bool attachOnscreen = embedOnscreen || std::getenv("SF_FXGUI_ATTACH_ONSCREEN") != nullptr;
|
||||
std::cout << "[FxGui] env SF_FXGUI_ATTACH_ONSCREEN=" << (attachOnscreen ? 1 : 0)
|
||||
<< " SF_FXGUI_EMBED_ONSCREEN=" << (embedOnscreen ? 1 : 0)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user