From 819257a1b33cc74c7585f156c41436c766cfbb72 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Wed, 19 Aug 2026 21:55:48 +0700 Subject: [PATCH] Fix bypass JSON bool + load SF2 tracks into bridge on restore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RenderFxJob: parse bypass as JSON bool (memberBool) — bypass:true previously ignored (memberInt parses numbers only), plugin kept processing. Verified: bypass RMS == plain, VST3 loaded once. - preloadTrackInstruments / bridge reconnect: load SF2 instruments into bridge too (loadAnyTrackToBridge) — opened projects with SF2 tracks sent MIDI to empty bridge channels (C++ silent), never reaching the native mastering chain. - rebuild app.precompiled.js from app.jsx (was stale: missing A14). --- app/static/js/app.jsx | 38 +++++++++++++++++++++++++++---- app/static/js/app.precompiled.js | 21 +++++++++++++---- native_bridge/src/RenderFxJob.cpp | 2 +- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 73724c2..1598567 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -6610,6 +6610,36 @@ const loadVstToBridge = async (track) => { return !!ok; } catch (e) { console.warn('[Bridge] loadVstToBridge fail:', track.id, e); return false; } }; +// D12: load track instrument (VST *hoặc* SF2) vào bridge. preloadTrackInstruments +// trước đây chỉ load VST → mở project có SF2 track + bridge active → MIDI tới +// channel bridge rỗng → C++ silent (không qua mastering chain native). +const loadAnyTrackToBridge = async (track) => { + if (!window.NativeBridgeService || !window.SonicMidiRouter || !window.SonicMidiRouter.isBridgeActive()) return false; + const inst = track.instrumentId || (track.synth_engine && track.synth_engine.plugin_id); + if (!inst) return false; + if (typeof inst === 'string' && inst.startsWith('sf_')) { + const se = track.synth_engine || {}; + const bank = se.soundfont_bank !== undefined ? se.soundfont_bank : (track.soundfont_bank !== undefined ? track.soundfont_bank : 0); + const bch = window.SonicMidiRouter.allocateChannel(track.id || track.trackId, bank === 128); + try { + const sfId = inst.replace('sf_', ''); + let bpath = null; + try { + const pl = await window.SonicAPI.listPlugins(); + const s0 = ((pl && pl.soundfonts) || []).find(s => String(s.id).replace('sf_', '') === sfId); + bpath = s0 && (s0.file || s0.path); + } catch (e) {} + const r = await window.SonicAPI.bridgeLoad({ name: inst, path: bpath || null, instrumentType: 'SF2', channel: bch }); + const p = r && r.path ? r.path : null; + if (!p) { console.warn('[Bridge] SF2 resolve fail:', track.id, inst); return false; } + const ok = await window.NativeBridgeService.loadInstrument(p, 'SF2', bch); + if (ok) { if (!window.__bridgeLoadedChannels) window.__bridgeLoadedChannels = {}; window.__bridgeLoadedChannels[bch] = true; if (!window.__bridgeLoadedPaths) window.__bridgeLoadedPaths = {}; window.__bridgeLoadedPaths[bch] = p; } + console.log('[Bridge] preload SF2 track', track.id, '-> ch', bch, ok ? 'OK' : 'FAIL', p); + return !!ok; + } catch (e) { console.warn('[Bridge] preload SF2 fail:', track.id, e); return false; } + } + try { return await loadVstToBridge(track); } catch (e) { return false; } +}; const ensureAndOpenVstGui = async (track) => { const trackId = track.id || track.trackId; const instrumentId = track.instrumentId || (track.synth_engine && track.synth_engine.plugin_id); @@ -6730,8 +6760,8 @@ const preloadTrackInstruments = async (tracks) => { for (let i = 0; i < list.length; i++) { const t = list[i]; const inst = t.instrumentId || (t.synth_engine && t.synth_engine.plugin_id); - if (!inst || (typeof inst === 'string' && inst.startsWith('sf_'))) continue; - try { await loadVstToBridge(t); } catch (e) {} + if (!inst) continue; + try { await loadAnyTrackToBridge(t); } catch (e) {} } } }; @@ -18136,8 +18166,8 @@ const App = () => { for (let i = 0; i < _tl.length; i++) { const _t = _tl[i]; const _inst = _t.instrumentId || (_t.synth_engine && _t.synth_engine.plugin_id); - if (!_inst || (typeof _inst === 'string' && _inst.startsWith('sf_'))) continue; - loadVstToBridge(_t).catch(function () {}); + if (!_inst) continue; + loadAnyTrackToBridge(_t).catch(function () {}); } } catch (e) { console.warn('[Bridge] reconnect reload fail:', e); } }); diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 98664a3..f1c8c94 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -302,7 +302,13 @@ try{masterBus.inputAnalyser.disconnect();masterBus.maximizerCompressor.disconnec // → global silence. The React effect applies it once per settings change. if(window.SonicSF&&window.SonicSF.init){window.SonicSF.init(audioCtx);}// C6: bootstrap native bridge 1 lần — query status, nối audio sink + router. if(window.NativeBridgeService&&window.SonicMidiRouter&&!window.__bridgeBootstrapped){window.__bridgeBootstrapped=true;window.SonicMidiRouter.onFallback=function(cmd,ch,pitch,vel){if(!window.SonicSF)return;if(cmd==='PANIC'){if(window.SonicSF.stopAll)window.SonicSF.stopAll();return;}if(cmd==='NOTE_ON')window.SonicSF.playNote(pitch,vel,undefined,undefined,undefined,undefined,ch);else window.SonicSF.stopNote(ch,pitch);};(async function(){try{var st=await window.NativeBridgeService.queryStatus();var connected=!!(st&&st.connected);window.SonicMidiRouter.setBridgeConnected(connected);console.log('[Bridge] bootstrap connected=',connected);if(connected){try{console.log('[Bridge] Requesting restart to match sampleRate:',audioCtx.sampleRate);await window.__TAURI__.core.invoke('restart_bridge_with_sample_rate',{sampleRate:audioCtx.sampleRate});// Bridge restart → mất hết instrument → reset map để reconnect reload. -window.__bridgeLoadedChannels={};window.__bridgeLoadedPaths={};for(var retry=0;retry<10;retry++){await new Promise(r=>setTimeout(r,100));var st2=await window.NativeBridgeService.queryStatus();if(st2&&st2.connected){console.log('[Bridge] Reconnected at sampleRate:',audioCtx.sampleRate);break;}}}catch(err){console.warn('[Bridge] restart error:',err);}window.BridgeAudioNode.init(audioCtx);window.NativeBridgeService.onAudio(function(l,r){window.BridgeAudioNode.onAudio(l,r);});if(window.AudioRoutingEngine)window.AudioRoutingEngine.connect(window.BridgeAudioNode,null,null);}}catch(e){console.warn('[Bridge] bootstrap error:',e);window.SonicMidiRouter.setBridgeConnected(false);}})();}return audioCtx;}const formatTime=secs=>{if(isNaN(secs)||secs<0)return"0:00.000";const m=Math.floor(secs/60);const s=Math.floor(secs%60);const ms=Math.floor(secs%1*1000).toString().padStart(3,'0');return`${m}:${s.toString().padStart(2,'0')}.${ms}`;};const formatTimeSimple=secs=>{if(isNaN(secs)||secs<0)return"0.00s";return`${secs.toFixed(2)}s`;};const formatBeat=(secs,bpmVal)=>{if(isNaN(secs)||secs<0)return"0.1.1";const beatDuration=60/bpmVal;const barDuration=beatDuration*4;const bar=Math.floor(secs/barDuration);const beat=Math.floor(secs%barDuration/beatDuration)+1;const sub=Math.floor(secs%beatDuration/(beatDuration/4))+1;return`${bar}.${beat}.${sub}`;};const midiPitchToName=pitch=>{const names=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;return names[pitch%12]+octave;};const getBeatMarkers=(maxDur,bpmVal)=>{const beatDuration=60/bpmVal;const barDuration=beatDuration*4;const markers=[];for(let t=0;t<=maxDur;t+=beatDuration){const isBar=Math.abs(t%barDuration)<0.001||Math.abs(t%barDuration-barDuration)<0.001;markers.push({time:t,isBar,beatNum:Math.floor(t/beatDuration)+1});}return markers;};const findZeroCrossing=(buffer,targetTime)=>{if(!buffer)return targetTime;const sampleRate=buffer.sampleRate;const data=buffer.getChannelData(0);const targetSample=Math.floor(targetTime*sampleRate);const windowSize=Math.floor(0.04*sampleRate);const start=Math.max(0,targetSample-windowSize);const end=Math.min(data.length-2,targetSample+windowSize);let bestSample=targetSample;let minDistance=Infinity;for(let i=start;i<=end;i++){if(data[i]>=0&&data[i+1]<=0||data[i]<=0&&data[i+1]>=0){const dist=Math.abs(i-targetSample);if(dist { noteId, startBeat, velocity } +window.__bridgeLoadedChannels={};window.__bridgeLoadedPaths={};for(var retry=0;retry<10;retry++){await new Promise(r=>setTimeout(r,100));var st2=await window.NativeBridgeService.queryStatus();if(st2&&st2.connected){console.log('[Bridge] Reconnected at sampleRate:',audioCtx.sampleRate);break;}}}catch(err){console.warn('[Bridge] restart error:',err);}window.BridgeAudioNode.init(audioCtx);window.NativeBridgeService.onAudio(function(l,r){window.BridgeAudioNode.onAudio(l,r);});// A14: plugin MIDI output -> route notes to the selected track +// (Scaler2/Instacomposer chords play the selected track's +// instrument). Feedback guard: skip events whose source channel +// is the selected track's own channel (would loop generator into +// itself). command is the VST2 status high nibble (0x90 etc.). +window.NativeBridgeService.onMidiOut(function(events){if(!events||!events.length)return;var ref=window.__selectedTrackIdRef;var tId=ref&&ref.current?ref.current:'1';for(var i=0;i0?'NOTE_ON':'NOTE_OFF';break;case 0x80:cmd='NOTE_OFF';break;case 0xB0:cmd='CC';break;case 0xC0:cmd='PROGRAM';break;case 0xE0:cmd='PITCH_BEND';break;default:continue;}if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){var tCh=window.SonicMidiRouter.allocateChannel(tId,false);if(tCh===ev.channel)continue;// feedback guard +window.SonicMidiRouter.pushEvent({cmd:cmd,channel:tId,pitch:ev.pitch,velocity:ev.velocity/127,data2:ev.data2,data3:ev.data3,sampleOffset:ev.sample_offset||0,live:true});}console.log('[Bridge] MIDI-out',cmd,'ch',ev.channel,'note',ev.pitch,'vel',ev.velocity,'-> track',tId);}});if(window.AudioRoutingEngine)window.AudioRoutingEngine.connect(window.BridgeAudioNode,null,null);}}catch(e){console.warn('[Bridge] bootstrap error:',e);window.SonicMidiRouter.setBridgeConnected(false);}})();}return audioCtx;}const formatTime=secs=>{if(isNaN(secs)||secs<0)return"0:00.000";const m=Math.floor(secs/60);const s=Math.floor(secs%60);const ms=Math.floor(secs%1*1000).toString().padStart(3,'0');return`${m}:${s.toString().padStart(2,'0')}.${ms}`;};const formatTimeSimple=secs=>{if(isNaN(secs)||secs<0)return"0.00s";return`${secs.toFixed(2)}s`;};const formatBeat=(secs,bpmVal)=>{if(isNaN(secs)||secs<0)return"0.1.1";const beatDuration=60/bpmVal;const barDuration=beatDuration*4;const bar=Math.floor(secs/barDuration);const beat=Math.floor(secs%barDuration/beatDuration)+1;const sub=Math.floor(secs%beatDuration/(beatDuration/4))+1;return`${bar}.${beat}.${sub}`;};const midiPitchToName=pitch=>{const names=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;return names[pitch%12]+octave;};const getBeatMarkers=(maxDur,bpmVal)=>{const beatDuration=60/bpmVal;const barDuration=beatDuration*4;const markers=[];for(let t=0;t<=maxDur;t+=beatDuration){const isBar=Math.abs(t%barDuration)<0.001||Math.abs(t%barDuration-barDuration)<0.001;markers.push({time:t,isBar,beatNum:Math.floor(t/beatDuration)+1});}return markers;};const findZeroCrossing=(buffer,targetTime)=>{if(!buffer)return targetTime;const sampleRate=buffer.sampleRate;const data=buffer.getChannelData(0);const targetSample=Math.floor(targetTime*sampleRate);const windowSize=Math.floor(0.04*sampleRate);const start=Math.max(0,targetSample-windowSize);const end=Math.min(data.length-2,targetSample+windowSize);let bestSample=targetSample;let minDistance=Infinity;for(let i=start;i<=end;i++){if(data[i]>=0&&data[i+1]<=0||data[i]<=0&&data[i+1]>=0){const dist=Math.abs(i-targetSample);if(dist { noteId, startBeat, velocity } this.recordedNotes=[];this.recStartAudioTime=0.0;this.recStartBar=0.0;this.selectedMidiInputId=null;// Compute round-trip browser latency this.latencyCompSec=(this.audioCtx.baseLatency||0)+(this.audioCtx.outputLatency||0);}start(startBar=0.0,selectedMidiInputId=null){this.isRecording=true;this.recordedNotes=[];this.activeNotes.clear();this.recStartBar=startBar;this.recStartAudioTime=this.audioCtx.currentTime;this.selectedMidiInputId=selectedMidiInputId;}handleMIDIMessage(event,sourceInputId=null){if(!this.isRecording)return;if(this.selectedMidiInputId&&this.selectedMidiInputId!=='ALL'&&sourceInputId&&sourceInputId!==this.selectedMidiInputId){console.log(`[DevLog] [MIDI Rec] Ignoring input message from "${sourceInputId}" (Selected: "${this.selectedMidiInputId}")`);return;}const[status,pitch,velocity]=event.data;const command=status>>4;// Apply latency compensation formula const currentTimeSec=Math.max(0,this.audioCtx.currentTime-this.recStartAudioTime-this.latencyCompSec);const secondsPerBeat=60.0/this.bpm;const currentBeat=currentTimeSec/secondsPerBeat;// Command 0x9: Note On @@ -518,7 +524,10 @@ if(!knownPath&&window.__bridgeLoadedState){const lc=window.__bridgeLoadedState.f if(knownPath&&window.__bridgeLoadedPaths[bch]===knownPath)return true;// Có path đáng tin → load thẳng qua Tauri invoke (không cần POST/auth/CORS). if(knownPath){const ok=await window.NativeBridgeService.loadInstrument(knownPath,btype,bch);if(ok){window.__bridgeLoadedChannels[bch]=true;window.__bridgeLoadedPaths[bch]=knownPath;if(!window.__bridgePluginPaths)window.__bridgePluginPaths={};window.__bridgePluginPaths[instrumentId]=knownPath;}console.log('[Bridge] auto-load track',track.id,'->',btype,'ch',bch,ok?'OK':'FAIL',knownPath);return!!ok;}// Không có path → nhờ engine resolve theo name (POST). Fail (auth/CORS) → // fallback listPlugins bên dưới. -const r=await window.SonicAPI.bridgeLoad({name:instrumentId,path:null,instrumentType:btype,channel:bch});let p=r&&r.path?r.path:null;if(!p&&window.SonicAPI.listPlugins){try{const pl=await window.SonicAPI.listPlugins();const v=(pl&&pl.vst_instruments||[]).find(x=>x.id===instrumentId||x.name===instrumentId);if(v&&v.path){p=v.path;if(v&&v.type)btype=v.type;if(!window.__bridgePluginPaths)window.__bridgePluginPaths={};window.__bridgePluginPaths[instrumentId]=v.path;}}catch(e2){console.warn('[Bridge] listPlugins fallback fail:',e2);}}if(!p)return false;const ok=await window.NativeBridgeService.loadInstrument(p,btype,bch);if(ok){window.__bridgeLoadedChannels[bch]=true;window.__bridgeLoadedPaths[bch]=p;}console.log('[Bridge] auto-load track',track.id,'->',btype,'ch',bch,ok?'OK':'FAIL',p);return!!ok;}catch(e){console.warn('[Bridge] loadVstToBridge fail:',track.id,e);return false;}};const ensureAndOpenVstGui=async track=>{const trackId=track.id||track.trackId;const instrumentId=track.instrumentId||track.synth_engine&&track.synth_engine.plugin_id;if(!instrumentId||typeof instrumentId==='string'&&instrumentId.startsWith('sf_'))return;if(!window.__bridgeLoadedChannels)window.__bridgeLoadedChannels={};let bch=window.SonicMidiRouter&&window.SonicMidiRouter.allocateChannel?window.SonicMidiRouter.allocateChannel(trackId,false):0;// LAN/standalone (browser không Tauri): NativeBridgeService chỉ log, không +const r=await window.SonicAPI.bridgeLoad({name:instrumentId,path:null,instrumentType:btype,channel:bch});let p=r&&r.path?r.path:null;if(!p&&window.SonicAPI.listPlugins){try{const pl=await window.SonicAPI.listPlugins();const v=(pl&&pl.vst_instruments||[]).find(x=>x.id===instrumentId||x.name===instrumentId);if(v&&v.path){p=v.path;if(v&&v.type)btype=v.type;if(!window.__bridgePluginPaths)window.__bridgePluginPaths={};window.__bridgePluginPaths[instrumentId]=v.path;}}catch(e2){console.warn('[Bridge] listPlugins fallback fail:',e2);}}if(!p)return false;const ok=await window.NativeBridgeService.loadInstrument(p,btype,bch);if(ok){window.__bridgeLoadedChannels[bch]=true;window.__bridgeLoadedPaths[bch]=p;}console.log('[Bridge] auto-load track',track.id,'->',btype,'ch',bch,ok?'OK':'FAIL',p);return!!ok;}catch(e){console.warn('[Bridge] loadVstToBridge fail:',track.id,e);return false;}};// D12: load track instrument (VST *hoặc* SF2) vào bridge. preloadTrackInstruments +// trước đây chỉ load VST → mở project có SF2 track + bridge active → MIDI tới +// channel bridge rỗng → C++ silent (không qua mastering chain native). +const loadAnyTrackToBridge=async track=>{if(!window.NativeBridgeService||!window.SonicMidiRouter||!window.SonicMidiRouter.isBridgeActive())return false;const inst=track.instrumentId||track.synth_engine&&track.synth_engine.plugin_id;if(!inst)return false;if(typeof inst==='string'&&inst.startsWith('sf_')){const se=track.synth_engine||{};const bank=se.soundfont_bank!==undefined?se.soundfont_bank:track.soundfont_bank!==undefined?track.soundfont_bank:0;const bch=window.SonicMidiRouter.allocateChannel(track.id||track.trackId,bank===128);try{const sfId=inst.replace('sf_','');let bpath=null;try{const pl=await window.SonicAPI.listPlugins();const s0=(pl&&pl.soundfonts||[]).find(s=>String(s.id).replace('sf_','')===sfId);bpath=s0&&(s0.file||s0.path);}catch(e){}const r=await window.SonicAPI.bridgeLoad({name:inst,path:bpath||null,instrumentType:'SF2',channel:bch});const p=r&&r.path?r.path:null;if(!p){console.warn('[Bridge] SF2 resolve fail:',track.id,inst);return false;}const ok=await window.NativeBridgeService.loadInstrument(p,'SF2',bch);if(ok){if(!window.__bridgeLoadedChannels)window.__bridgeLoadedChannels={};window.__bridgeLoadedChannels[bch]=true;if(!window.__bridgeLoadedPaths)window.__bridgeLoadedPaths={};window.__bridgeLoadedPaths[bch]=p;}console.log('[Bridge] preload SF2 track',track.id,'-> ch',bch,ok?'OK':'FAIL',p);return!!ok;}catch(e){console.warn('[Bridge] preload SF2 fail:',track.id,e);return false;}}try{return await loadVstToBridge(track);}catch(e){return false;}};const ensureAndOpenVstGui=async track=>{const trackId=track.id||track.trackId;const instrumentId=track.instrumentId||track.synth_engine&&track.synth_engine.plugin_id;if(!instrumentId||typeof instrumentId==='string'&&instrumentId.startsWith('sf_'))return;if(!window.__bridgeLoadedChannels)window.__bridgeLoadedChannels={};let bch=window.SonicMidiRouter&&window.SonicMidiRouter.allocateChannel?window.SonicMidiRouter.allocateChannel(trackId,false):0;// LAN/standalone (browser không Tauri): NativeBridgeService chỉ log, không // invoke được open_vst_gui → nhờ engine đẩy LOAD+OPEN_GUI vào SHM qua route // /bridge/gui (E6). GUI hiện trên desktop máy chạy server. if(!window.SonicMidiRouter||!window.SonicMidiRouter.isBridgeActive()||!window.NativeBridgeService){try{if(!window.SonicAPI||!window.SonicAPI.bridgeOpenGui)return;const r=await window.SonicAPI.bridgeOpenGui({name:instrumentId,path:null,instrumentType:null,channel:bch});if(r&&r.success)console.log('[Bridge] GUI open via API',instrumentId,'ch',bch,r.path);else console.warn('[Bridge] GUI via API response:',r);}catch(e){console.warn('[Bridge] GUI via API fail:',trackId,e);}return;}// V9: load (nếu chưa có ĐÚNG instrument) rồi CHỈ mở GUI khi load OK — không @@ -559,7 +568,7 @@ const c=(_wheelAcc.get(key)||0)-norm;const steps=Math.trunc(c/pxPerStep);_wheelA // warm the FluidSynth font cache for each track's instrument. This only loads // the soundfonts; the per-track channel re-selection happens at note time. const preloadTrackInstruments=async tracks=>{const list=tracks||[];if(window.SonicSF&&window.SonicSF.selectInstrument){for(let i=0;i{if(!isOpen)return null;const[activeTab,setActiveTab]=useState('account');const[profile,setProfile]=useState(null);const[oldPassword,setOldPassword]=useState('');const[newPassword,setNewPassword]=useState('');const[msg,setMsg]=useState('');const[error,setError]=useState('');const[loading,setLoading]=useState(false);const[dragOfs,setDragOfs]=useState({x:0,y:0});const dragRef=useRef({active:false,startX:0,startY:0,ofsX:0,ofsY:0});// Projects list state +if(window.NativeBridgeService&&window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){for(let i=0;i{if(!isOpen)return null;const[activeTab,setActiveTab]=useState('account');const[profile,setProfile]=useState(null);const[oldPassword,setOldPassword]=useState('');const[newPassword,setNewPassword]=useState('');const[msg,setMsg]=useState('');const[error,setError]=useState('');const[loading,setLoading]=useState(false);const[dragOfs,setDragOfs]=useState({x:0,y:0});const dragRef=useRef({active:false,startX:0,startY:0,ofsX:0,ofsY:0});// Projects list state const[projectsList,setProjectsList]=useState([]);const[loadingProjects,setLoadingProjects]=useState(false);// Files list state const[filesList,setFilesList]=useState([]);const[loadingFiles,setLoadingFiles]=useState(false);// Confirmation modal state const[confirmModal,setConfirmModal]=useState(null);// Backup state @@ -1170,7 +1179,7 @@ window.__bridgeLoadedChannels={};window.__bridgeLoadedPaths={};// FIX auto-play: // → master-silence watchdog tu resume phat lai → am tu dong. Stop ngay // de page khong con "dang phat" — user muon nghe thi bam play. try{stopAllPlayback();setIsPlaying(false);}catch(e){console.warn('[Bridge] stop-on-down fail:',e);}return;}// Bridge (re)connect → load lại toàn bộ VST track (không mở GUI). -try{const _tl=activeTracksRef.current||[];for(let i=0;i<_tl.length;i++){const _t=_tl[i];const _inst=_t.instrumentId||_t.synth_engine&&_t.synth_engine.plugin_id;if(!_inst||typeof _inst==='string'&&_inst.startsWith('sf_'))continue;loadVstToBridge(_t).catch(function(){});}}catch(e){console.warn('[Bridge] reconnect reload fail:',e);}});return()=>window.NativeBridgeService.onStatusChange(null);},[]);const loadAudioBuffersForTracks=async tracksList=>{let hasLoadedAny=false;const loadBuffer=async url=>{const res=await fetch(url);if(!res.ok)return null;const blob=await res.blob();return await window.SonicAudio.decodeAudioFile(blob);};const tryLoad=async fileId=>{if(!fileId)return null;try{const result=await loadBuffer('/static/audio/uploads/'+fileId);if(result)return result;}catch(_){}try{const result=await loadBuffer(`${API_AUDIO}/download/${fileId}`);if(result)return result;}catch(_){}return null;};const updatedTracks=await Promise.all(tracksList.map(async t=>{let trackBuffer=t.buffer;let trackChannelInfo=t.channelInfo;if(t.serverFileId&&!trackBuffer){const result=await tryLoad(t.serverFileId);if(result){trackBuffer=result.audioBuffer;trackChannelInfo=result.channelInfo;hasLoadedAny=true;}}const updatedClips=await Promise.all((t.clips||[]).map(async c=>{let clipBuffer=c.buffer;const targetFileId=c.serverFileId||t.serverFileId;if(targetFileId&&!clipBuffer){const result=await tryLoad(targetFileId);if(result){clipBuffer=result.audioBuffer;hasLoadedAny=true;}}return{...c,buffer:clipBuffer};}));// Section items: nạp CẢ audioclip bên trong section (trước đây bỏ sót → +try{const _tl=activeTracksRef.current||[];for(let i=0;i<_tl.length;i++){const _t=_tl[i];const _inst=_t.instrumentId||_t.synth_engine&&_t.synth_engine.plugin_id;if(!_inst)continue;loadAnyTrackToBridge(_t).catch(function(){});}}catch(e){console.warn('[Bridge] reconnect reload fail:',e);}});return()=>window.NativeBridgeService.onStatusChange(null);},[]);const loadAudioBuffersForTracks=async tracksList=>{let hasLoadedAny=false;const loadBuffer=async url=>{const res=await fetch(url);if(!res.ok)return null;const blob=await res.blob();return await window.SonicAudio.decodeAudioFile(blob);};const tryLoad=async fileId=>{if(!fileId)return null;try{const result=await loadBuffer('/static/audio/uploads/'+fileId);if(result)return result;}catch(_){}try{const result=await loadBuffer(`${API_AUDIO}/download/${fileId}`);if(result)return result;}catch(_){}return null;};const updatedTracks=await Promise.all(tracksList.map(async t=>{let trackBuffer=t.buffer;let trackChannelInfo=t.channelInfo;if(t.serverFileId&&!trackBuffer){const result=await tryLoad(t.serverFileId);if(result){trackBuffer=result.audioBuffer;trackChannelInfo=result.channelInfo;hasLoadedAny=true;}}const updatedClips=await Promise.all((t.clips||[]).map(async c=>{let clipBuffer=c.buffer;const targetFileId=c.serverFileId||t.serverFileId;if(targetFileId&&!clipBuffer){const result=await tryLoad(targetFileId);if(result){clipBuffer=result.audioBuffer;hasLoadedAny=true;}}return{...c,buffer:clipBuffer};}));// Section items: nạp CẢ audioclip bên trong section (trước đây bỏ sót → // audioclip trong section hiện "(audio chưa được tải)" sau save+open). const updatedSections=await Promise.all((t.sections||[]).map(async s=>{const updatedSTracks=await Promise.all((s.tracks||[]).map(async st=>{const updatedSClips=await Promise.all((st.clips||[]).map(async c=>{let clipBuffer=c.buffer;const targetFileId=c.serverFileId||st.serverFileId;if(targetFileId&&!clipBuffer){const result=await tryLoad(targetFileId);if(result){clipBuffer=result.audioBuffer;hasLoadedAny=true;}}return{...c,buffer:clipBuffer};}));return{...st,clips:updatedSClips};}));return{...s,tracks:updatedSTracks};}));if(trackBuffer&&updatedClips.length===0){const clipId=`default_${t.id}`;return{...t,buffer:trackBuffer,channelInfo:trackChannelInfo,clips:[{id:clipId,buffer:trackBuffer,startTime:t.startTime||0,name:t.name,speed:1.0}],sections:updatedSections};}return{...t,buffer:trackBuffer,channelInfo:trackChannelInfo,clips:updatedClips,sections:updatedSections};}));if(hasLoadedAny){setTracks(prev=>{// Merge by TRACK ID (not array index): if the state changed between the // fetch start and now (e.g. another project opened), index-based merging @@ -1244,7 +1253,9 @@ setSubTabs(prev=>{const next=prev.filter(s=>!(s.target_id&&idsToDelete.has(s.tar // KHÔNG đá SECTION-TAB về main (user 07:45) if(activeTabRef.current&&activeTabRef.current.startsWith('midi_')&&!next.some(s=>s.id===activeTabRef.current)){setActiveTab('main');}return next;});// Sync nội dung section tab → section item trên MAIN (canvas vẽ lại — // user 07:45) — setTimeout 0: chạy SAU updateActiveTracks (data mới) -if(activeTabRef.current&&activeTabRef.current.startsWith('session_')){const _tabId=activeTabRef.current;setTimeout(()=>{try{syncSectionTabToMain(_tabId);}catch(e){}},0);}setSelectedItemIds(new Set());updateActiveTracks(prev=>prev.map(t=>{let changed=false;const sections=(t.sections||[]).filter(s=>{if(idsToDelete.has(s.id)){changed=true;return false;}return true;});const midiItems=(t.midiItems||[]).filter(m=>{if(idsToDelete.has(m.id)){changed=true;return false;}return true;});let clips=t.clips||[];const hasVirtualClip=!t.clips||t.clips.length===0;if(hasVirtualClip&&t.buffer){const canonical='default_'+t.id;if(idsToDelete.has(canonical)){changed=true;return{...t,sections,midiItems,clips:[],buffer:null,startTime:0};}}const updatedClips=clips.filter(c=>{const canonical=c.id==='default'?'default_'+t.id:c.id;if(idsToDelete.has(canonical)){changed=true;return false;}return true;});if(!changed&§ions.length===(t.sections||[]).length&&midiItems.length===(t.midiItems||[]).length){return t;}return{...t,sections,midiItems,clips:updatedClips,buffer:updatedClips.length>0?updatedClips[0].buffer:null,startTime:updatedClips.length>0?updatedClips[0].startTime:0,name:updatedClips.length>0?updatedClips[0].name:t.name};}));showToast(count===1?'Đã xóa 1 item.':`Đã xóa ${count} items.`,'info');};const selectedClipIdRef=useRef(null);selectedClipIdRef.current=selectedClipId;const selectedTrackIdRef=useRef(selectedTrackId);selectedTrackIdRef.current=selectedTrackId;const handleDeleteTrackRef=useRef(()=>{});const handleSplitTrackRef=useRef(()=>{});const handleRecordClickRef=useRef(()=>{});const activeTabRef=useRef(activeTab);activeTabRef.current=activeTab;const activePlaybackSpeedRef=useRef(1.0);const subTabsRef=useRef(subTabs);subTabsRef.current=subTabs;const subTabSelectedNodeTimeRef=useRef(null);subTabSelectedNodeTimeRef.current=subTabSelectedNodeTime;const handleSubTabNormalize=tabId=>{setSubTabs(prev=>prev.map(st=>{if(st.id!==tabId||!st.buffer)return st;const ctx=getAudioContext();const data=st.buffer.getChannelData(0);const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):0;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):st.buffer.duration;const startSample=Math.floor(left*st.buffer.sampleRate);const endSample=Math.floor(right*st.buffer.sampleRate);let maxVal=0;for(let i=startSample;imaxVal)maxVal=abs;}if(maxVal===0)return st;const scale=1.0/maxVal;const clonedBuffer=ctx.createBuffer(1,Math.max(1,data.length),st.buffer.sampleRate);const clonedData=clonedBuffer.getChannelData(0);clonedData.set(data);for(let i=startSample;i{setSubTabs(prev=>prev.map(st=>{if(st.id!==tabId||!st.buffer)return st;const ctx=getAudioContext();const data=st.buffer.getChannelData(0);const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):0;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):st.buffer.duration;const startSample=Math.floor(left*st.buffer.sampleRate);const endSample=Math.floor(right*st.buffer.sampleRate);const scale=Math.pow(10,gainDb/20);const clonedBuffer=ctx.createBuffer(1,Math.max(1,data.length),st.buffer.sampleRate);const clonedData=clonedBuffer.getChannelData(0);clonedData.set(data);for(let i=startSample;i{setSubTabs(prev=>prev.map(st=>{if(st.id!==tabId||!st.buffer)return st;const ctx=getAudioContext();const data=st.buffer.getChannelData(0);const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):0;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):st.buffer.duration;const startSample=Math.floor(left*st.buffer.sampleRate);const endSample=Math.floor(right*st.buffer.sampleRate);const durationSamples=endSample-startSample;if(durationSamples<=0)return st;const clonedBuffer=ctx.createBuffer(1,Math.max(1,data.length),st.buffer.sampleRate);const clonedData=clonedBuffer.getChannelData(0);clonedData.set(data);if(type==='in'){for(let i=0;i{const st=subTabsRef.current.find(s=>s.id===tabId);if(!st||!st.buffer)return;const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):null;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):null;if(left===null||right===null||left===right){showToast('Vui lòng chọn vùng để Cut.','warning');return;}const ctx=getAudioContext();const sr=st.buffer.sampleRate;const data=st.buffer.getChannelData(0);const startSample=Math.floor(left*sr);const endSample=Math.floor(right*sr);const len=Math.max(1,endSample-startSample);// guard 0 frames → createBuffer crash +if(activeTabRef.current&&activeTabRef.current.startsWith('session_')){const _tabId=activeTabRef.current;setTimeout(()=>{try{syncSectionTabToMain(_tabId);}catch(e){}},0);}setSelectedItemIds(new Set());updateActiveTracks(prev=>prev.map(t=>{let changed=false;const sections=(t.sections||[]).filter(s=>{if(idsToDelete.has(s.id)){changed=true;return false;}return true;});const midiItems=(t.midiItems||[]).filter(m=>{if(idsToDelete.has(m.id)){changed=true;return false;}return true;});let clips=t.clips||[];const hasVirtualClip=!t.clips||t.clips.length===0;if(hasVirtualClip&&t.buffer){const canonical='default_'+t.id;if(idsToDelete.has(canonical)){changed=true;return{...t,sections,midiItems,clips:[],buffer:null,startTime:0};}}const updatedClips=clips.filter(c=>{const canonical=c.id==='default'?'default_'+t.id:c.id;if(idsToDelete.has(canonical)){changed=true;return false;}return true;});if(!changed&§ions.length===(t.sections||[]).length&&midiItems.length===(t.midiItems||[]).length){return t;}return{...t,sections,midiItems,clips:updatedClips,buffer:updatedClips.length>0?updatedClips[0].buffer:null,startTime:updatedClips.length>0?updatedClips[0].startTime:0,name:updatedClips.length>0?updatedClips[0].name:t.name};}));showToast(count===1?'Đã xóa 1 item.':`Đã xóa ${count} items.`,'info');};const selectedClipIdRef=useRef(null);selectedClipIdRef.current=selectedClipId;const selectedTrackIdRef=useRef(selectedTrackId);selectedTrackIdRef.current=selectedTrackId;// A14: plugin MIDI-out routing target — bootstrap handler (module scope) +// reads the live selection through this window ref (avoid stale closure). +window.__selectedTrackIdRef=selectedTrackIdRef;const handleDeleteTrackRef=useRef(()=>{});const handleSplitTrackRef=useRef(()=>{});const handleRecordClickRef=useRef(()=>{});const activeTabRef=useRef(activeTab);activeTabRef.current=activeTab;const activePlaybackSpeedRef=useRef(1.0);const subTabsRef=useRef(subTabs);subTabsRef.current=subTabs;const subTabSelectedNodeTimeRef=useRef(null);subTabSelectedNodeTimeRef.current=subTabSelectedNodeTime;const handleSubTabNormalize=tabId=>{setSubTabs(prev=>prev.map(st=>{if(st.id!==tabId||!st.buffer)return st;const ctx=getAudioContext();const data=st.buffer.getChannelData(0);const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):0;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):st.buffer.duration;const startSample=Math.floor(left*st.buffer.sampleRate);const endSample=Math.floor(right*st.buffer.sampleRate);let maxVal=0;for(let i=startSample;imaxVal)maxVal=abs;}if(maxVal===0)return st;const scale=1.0/maxVal;const clonedBuffer=ctx.createBuffer(1,Math.max(1,data.length),st.buffer.sampleRate);const clonedData=clonedBuffer.getChannelData(0);clonedData.set(data);for(let i=startSample;i{setSubTabs(prev=>prev.map(st=>{if(st.id!==tabId||!st.buffer)return st;const ctx=getAudioContext();const data=st.buffer.getChannelData(0);const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):0;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):st.buffer.duration;const startSample=Math.floor(left*st.buffer.sampleRate);const endSample=Math.floor(right*st.buffer.sampleRate);const scale=Math.pow(10,gainDb/20);const clonedBuffer=ctx.createBuffer(1,Math.max(1,data.length),st.buffer.sampleRate);const clonedData=clonedBuffer.getChannelData(0);clonedData.set(data);for(let i=startSample;i{setSubTabs(prev=>prev.map(st=>{if(st.id!==tabId||!st.buffer)return st;const ctx=getAudioContext();const data=st.buffer.getChannelData(0);const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):0;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):st.buffer.duration;const startSample=Math.floor(left*st.buffer.sampleRate);const endSample=Math.floor(right*st.buffer.sampleRate);const durationSamples=endSample-startSample;if(durationSamples<=0)return st;const clonedBuffer=ctx.createBuffer(1,Math.max(1,data.length),st.buffer.sampleRate);const clonedData=clonedBuffer.getChannelData(0);clonedData.set(data);if(type==='in'){for(let i=0;i{const st=subTabsRef.current.find(s=>s.id===tabId);if(!st||!st.buffer)return;const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):null;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):null;if(left===null||right===null||left===right){showToast('Vui lòng chọn vùng để Cut.','warning');return;}const ctx=getAudioContext();const sr=st.buffer.sampleRate;const data=st.buffer.getChannelData(0);const startSample=Math.floor(left*sr);const endSample=Math.floor(right*sr);const len=Math.max(1,endSample-startSample);// guard 0 frames → createBuffer crash const cutBuffer=ctx.createBuffer(1,len,sr);cutBuffer.copyToChannel(data.subarray(startSample,endSample),0);clipboardRef.current={buffer:cutBuffer,name:'Subtab Clip'};const newBuffer=ctx.createBuffer(1,Math.max(1,data.length-len),sr);const newData=newBuffer.getChannelData(0);let idx=0;for(let i=0;iprev.map(s=>s.id===tabId?{...s,buffer:newBuffer,currentTime:left,selectionStart:null,selectionEnd:null}:s));showToast('Đã Cut vùng chọn.','success');};const handleSubTabCopy=tabId=>{const st=subTabsRef.current.find(s=>s.id===tabId);if(!st||!st.buffer)return;const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):null;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):null;if(left===null||right===null||left===right){showToast('Vui lòng chọn vùng để Copy.','warning');return;}const ctx=getAudioContext();const sr=st.buffer.sampleRate;const data=st.buffer.getChannelData(0);const startSample=Math.floor(left*sr);const endSample=Math.floor(right*sr);const len=Math.max(1,endSample-startSample);// guard 0 frames → createBuffer crash const copyBuffer=ctx.createBuffer(1,len,sr);copyBuffer.copyToChannel(data.subarray(startSample,endSample),0);clipboardRef.current={buffer:copyBuffer,name:'Subtab Clip',sampleRate:sr,channels:1,speed:1.0};showToast('Đã Copy vùng chọn.','success');};const handleSubTabPaste=tabId=>{const st=subTabsRef.current.find(s=>s.id===tabId);if(!st)return;// Check if Piano Roll Tab if(st.type==='PIANO_ROLL'){const clip=clipboardRef.current||window.globalStudioClipboard;if(!clip||clip.type!=='midi'||!clip.notes){showToast('Clipboard không chứa dữ liệu MIDI.','warning');return;}const secondsPerBeat=60/(bpm||120);const pasteBeat=(st.currentTime||0)/secondsPerBeat;const newNotes=clip.notes.map(n=>({id:'note_'+Math.random().toString(36).substr(2,9),pitch:n.pitch,start_beat:pasteBeat+n.start_beat,duration_beats:n.duration_beats,velocity:n.velocity||0.8}));setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,notes:[...(s.notes||[]),...newNotes]}:s));showToast('Đã dán note MIDI vào Piano Roll.','success');return;}// Default Waveform Audio Paste diff --git a/native_bridge/src/RenderFxJob.cpp b/native_bridge/src/RenderFxJob.cpp index bcdc44e..b01d034 100644 --- a/native_bridge/src/RenderFxJob.cpp +++ b/native_bridge/src/RenderFxJob.cpp @@ -1104,7 +1104,7 @@ std::shared_ptr RealtimeFxChain::Impl::buildChain( const std::string path = memberString(o, "path", ""); if (path.empty()) continue; Entry ent; - ent.bypass = memberInt(o, "bypass", 0) != 0; + ent.bypass = memberBool(o, "bypass", false); if (!ent.bypass) { auto fx = std::make_shared(); if (fx->load(path, sampleRate_, blockSize_)) {