From d168328004ab9f39a533df5f4193412a7a5797b9 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Thu, 13 Aug 2026 11:26:39 +0700 Subject: [PATCH] Fix AI proxy origin in Tauri, SF2 program_select, VSTi GUI reopen + sample rate bridge - aiGateway.js: use window.API_BASE_URL (engine port) instead of window.location.origin (tauri://localhost asset protocol returned index.html -> JSON parse error); check content-type before parsing response - NativeInstrumentEngine.cpp: fluid_synth_program_select for SF2 instrument + program change - Vst3Instrument.cpp: activate all audio/event buses, channel=0 forced, GUI resize/reopen, kEvent using fix - main.cpp: VST GUI reopen via WM_DESTROY, OleInitialize, steady_clock loop - lib.rs: BridgeSampleRate state, restart_bridge_with_sample_rate command, borrow fix - app.jsx/nativeBridgeService.js: isMidiTrack widening, bridge masterbus connect, held MIDI notes, sample-rate restart, openNativeGUI channel --- app/static/js/app.jsx | 39 +++++++-- app/static/js/app.precompiled.js | 14 ++-- app/static/js/services/aiGateway.js | 18 +++- app/static/js/services/nativeBridgeService.js | 7 +- native_bridge/src/NativeInstrumentEngine.cpp | 5 +- native_bridge/src/Vst3Instrument.cpp | 55 +++++++++--- native_bridge/src/main.cpp | 83 ++++++++++++++----- src-tauri/src/lib.rs | 56 +++++++++++-- 8 files changed, 218 insertions(+), 59 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index d056476..9020223 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -1416,6 +1416,20 @@ function getAudioContext() { 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 }); + 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); @@ -15378,7 +15392,7 @@ const App = () => { // Requirement 2: chọn VST3 qua nút Synth → load xong mở native GUI // (C++ attach editor vào cửa sổ bridge tự tạo — control type=4, hwnd=0). if (ok && btype === 'VST3' && window.NativeBridgeService.openNativeGUI) { - try { await window.NativeBridgeService.openNativeGUI(instrumentId); } catch (e) {} + try { await window.NativeBridgeService.openNativeGUI(instrumentId, bch); } catch (e) {} } } })(); @@ -15647,6 +15661,12 @@ const App = () => { var bTracks = activeTracksRef.current || []; bTracks.forEach(function (bt) { if (!bt.isArmed) return; + if (window.triggerMidiVuActivity) { + window.triggerMidiVuActivity(bt.id, scaledVel); + } + try { + heldMidiNotesRef.current[bt.id] = (heldMidiNotesRef.current[bt.id] || 0) + 1; + } catch (err) {} try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: bt.id, pitch: pitch, velocity: scaledVel / 127, percussion: !!(bt.synth_engine && bt.synth_engine.soundfont_bank === 128) }); } catch (e) {} }); } else if (window.SonicSF) { @@ -15722,6 +15742,14 @@ const App = () => { // D1: bridge note-off qua router (channel trùng NOTE_ON — track.id). var bStopTracks = activeTracksRef.current || []; bStopTracks.forEach(function (bst) { + try { + if (heldMidiNotesRef.current[bst.id] !== undefined) { + heldMidiNotesRef.current[bst.id] = Math.max(0, heldMidiNotesRef.current[bst.id] - 1); + if (heldMidiNotesRef.current[bst.id] === 0) { + delete heldMidiNotesRef.current[bst.id]; + } + } + } catch (err) {} try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: bst.id, pitch: pitch, velocity: 0, percussion: !!(bst.synth_engine && bst.synth_engine.soundfont_bank === 128) }); } catch (e) {} }); } else if (window.SonicSF && window.SonicSF.stopNote) { @@ -21216,7 +21244,8 @@ const App = () => { // NEVER affects the soundfont instrument, and the ♪ bypass never touches // audio clips/sections. let sfEntry = null, sfOut = null, sfPan = null, sfAnalyser = null, sfRouteGain = null, sfDryGain = null, sfMods = []; - if ((track.midiItems && track.midiItems.length > 0)) { + const isMidiTrack = (track.midiItems && track.midiItems.length > 0) || track.type === 'MIDI' || !!track.instrumentId; + if (isMidiTrack) { sfEntry = context.createGain(); sfOut = context.createGain(); sfPan = context.createStereoPanner(); @@ -21401,7 +21430,7 @@ const App = () => { } } } - const midiAudible = list.filter(t => (t.midiItems && t.midiItems.length > 0) && computeTrackAudibleGain(list, t) > 0); + const midiAudible = list.filter(t => ((t.midiItems && t.midiItems.length > 0) || t.isArmed || t.type === 'MIDI' || !!t.instrumentId) && computeTrackAudibleGain(list, t) > 0); console.log('[SFRoute] activeTab=' + _activeTabId, 'midiAudible=' + midiAudible.map(t => t.id + '(midi' + (t.midiItems || []).length + ')').join(',') || 'none'); if (midiAudible.length === 1) { const t = midiAudible[0]; @@ -21454,8 +21483,8 @@ const App = () => { } // D5: bridge active + không track nào audible → ngắt khỏi graph (về // masterBus qua updateSfRouting lần sau khi có track). - if (window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) { - window.AudioRoutingEngine.disconnect(); + if (window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive() && window.BridgeAudioNode) { + window.AudioRoutingEngine.connect(window.BridgeAudioNode, null, null); } else if (window.SonicSF && window.SonicSF.setOutputDestination) { window.SonicSF.setOutputDestination(null); } diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 83e3252..6ab4e1b 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -247,7 +247,7 @@ try{masterBus.inputAnalyser.disconnect();masterBus.maximizerCompressor.disconnec // ("BiquadFilterNode: state is bad") and can leave the master routing broken // → 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){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 } +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});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 } 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 @@ -777,7 +777,7 @@ const[instrumentDropdownTrackId,setInstrumentDropdownTrackId]=useState(null);con // keybed dùng khi push NOTE_ON). Không load → MIDI tới channel rỗng → C++ silent. const loadTrackInstrumentToBridge=(trackId,instrumentId,bank)=>{if(!window.SonicMidiRouter||!window.SonicMidiRouter.isBridgeActive()||!instrumentId)return;const isSf=typeof instrumentId==='string'&&instrumentId.startsWith('sf_');const bch=window.SonicMidiRouter.allocateChannel(trackId,bank===128);const btype=isSf?'SF2':'VST3';let bpath=null;if(instrumentSelectorData){if(isSf){const sid=instrumentId.replace('sf_','');const s0=(instrumentSelectorData.soundfonts||[]).find(s=>String(s.id).replace('sf_','')===sid);bpath=s0&&(s0.file||s0.path);}else{const v0=(instrumentSelectorData.vst_instruments||[]).find(v=>v.id===instrumentId||v.name===instrumentId);bpath=v0&&v0.path;}}(async()=>{let p=bpath;try{const r=await window.SonicAPI.bridgeLoad({name:instrumentId,path:bpath||null,instrumentType:btype,channel:bch});if(r&&r.path)p=r.path;}catch(e){console.warn('[Bridge] resolve fail:',e);}if(p){const ok=await window.NativeBridgeService.loadInstrument(p,btype,bch);console.log('[Bridge] track',trackId,'-> bridge',btype,'ch',bch,ok?'OK':'FAIL',p);// Requirement 2: chọn VST3 qua nút Synth → load xong mở native GUI // (C++ attach editor vào cửa sổ bridge tự tạo — control type=4, hwnd=0). -if(ok&&btype==='VST3'&&window.NativeBridgeService.openNativeGUI){try{await window.NativeBridgeService.openNativeGUI(instrumentId);}catch(e){}}}})();};const setTrackInstrumentWithProgram=(trackId,instrumentId,programNumber,displayName,bankNumber)=>{const isSfInstrument=instrumentId&&typeof instrumentId==='string'&&instrumentId.startsWith('sf_');const sfBank=bankNumber!==undefined?bankNumber:isSfInstrument?0:undefined;const sfProg=programNumber!==undefined?programNumber:undefined;var mt=activeTracksRef.current||tracks;var curTrk=null;for(var ci=0;ci{const isSfInstrument=instrumentId&&typeof instrumentId==='string'&&instrumentId.startsWith('sf_');const sfBank=bankNumber!==undefined?bankNumber:isSfInstrument?0:undefined;const sfProg=programNumber!==undefined?programNumber:undefined;var mt=activeTracksRef.current||tracks;var curTrk=null;for(var ci=0;ciprev.map(t=>{if(t.id!==trackId)return t;const hasInstrument=!!instrumentId;const instrType=isSfInstrument?'soundfont':hasInstrument?'vst3':'default';const synthEngine=hasInstrument?{type:instrType,plugin_id:instrumentId,soundfont_bank:sfBank!==undefined?sfBank:0,soundfont_program:sfProg!==undefined?sfProg:0,soundfont_id:isSfInstrument?instrumentId.replace('sf_',''):''}:undefined;return{...t,midiChannel:mch,instrumentId,instrumentProgram:sfProg,instrumentName:displayName,soundfont_bank:sfBank,soundfont_program:sfProg,synth_engine:synthEngine,type:hasInstrument?'MIDI':t.type==='MIDI'?'audio':t.type};}));setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setSynthCategory(null);// Trigger FluidSynth load + program change when soundfont instrument selected @@ -815,7 +815,7 @@ try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch( // Do NOT use raw MIDI hardware channel (msg.data[0] & 0x0F) if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){// D1: bridge active -> 1 cổng router (per-track channel alloc) → // Rust SHM → C++ bridge; giữ SonicSF/Carla fallback ở nhánh else. -var bTracks=activeTracksRef.current||[];bTracks.forEach(function(bt){if(!bt.isArmed)return;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:bt.id,pitch:pitch,velocity:scaledVel/127,percussion:!!(bt.synth_engine&&bt.synth_engine.soundfont_bank===128)});}catch(e){}});}else if(window.SonicSF){var allTracks=activeTracksRef.current||[];var arSubs=subTabsRef&&subTabsRef.current?subTabsRef.current.filter(function(s){return s.type==='PIANO_ROLL'&&s.isArmed;}):[];var armedTracks=allTracks.filter(function(t){return t.isArmed;});// Piano Roll arming has priority: route to each armed sub-tab's parent track +var bTracks=activeTracksRef.current||[];bTracks.forEach(function(bt){if(!bt.isArmed)return;if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(bt.id,scaledVel);}try{heldMidiNotesRef.current[bt.id]=(heldMidiNotesRef.current[bt.id]||0)+1;}catch(err){}try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:bt.id,pitch:pitch,velocity:scaledVel/127,percussion:!!(bt.synth_engine&&bt.synth_engine.soundfont_bank===128)});}catch(e){}});}else if(window.SonicSF){var allTracks=activeTracksRef.current||[];var arSubs=subTabsRef&&subTabsRef.current?subTabsRef.current.filter(function(s){return s.type==='PIANO_ROLL'&&s.isArmed;}):[];var armedTracks=allTracks.filter(function(t){return t.isArmed;});// Piano Roll arming has priority: route to each armed sub-tab's parent track if(arSubs.length>0){arSubs.forEach(function(as){var asTrk=allTracks.find(function(t){return t.id===as.trackId;});var asCh=asTrk?assignTrackMidiChannel(asTrk,allTracks):0;var asProg=as.instrumentProgram;var asSe=as.synth_engine;if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(as.trackId,scaledVel);}// Giữ VU theo trường độ: tăng counter note đang giữ // (tick giữ peak cho tới khi note-off) try{heldMidiNotesRef.current[as.trackId]=(heldMidiNotesRef.current[as.trackId]||0)+1;}catch(err){}if(isStandaloneSf()&&isSfTrackEngine(asSe)&&!shouldRouteCarla(asSe)){playNativeSfNote(asTrk,pitch,scaledVel/127,60000,undefined,'midi_'+as.trackId+'_'+pitch);}else{window.SonicSF.playNote(pitch,scaledVel,60000,undefined,asProg,null,asCh,asSe);}// MIDI Keyboard → Carla bridge (piano roll sub-tab ARM): @@ -829,7 +829,7 @@ try{heldMidiNotesRef.current[at.id]=(heldMidiNotesRef.current[at.id]||0)+1;}catc if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(atSe,at.isArmed)){try{window.SonicCarlaMidi.noteOn(atCh,pitch,scaledVel);}catch(e){}}});}}else if(cmd===0x8||cmd===0x9&&rawVel===0){activeMidiPitchesRef.current.delete(pitch);setActiveMidiPitches(new Set(activeMidiPitchesRef.current));const current=lastMidiNoteRef.current;if(current&¤t.pitch===pitch){const lenSec=(performance.now()-current.startTime)/1000;lastMidiNoteRef.current={...current,length:lenSec};setLastMidiNote(prev=>prev&&prev.pitch===pitch?{...prev,length:lenSec,time:Date.now()}:prev);}// Stop the note on ALL tracks (not just armed) to prevent stuck notes // when ARM is toggled off while a key is held if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){// D1: bridge note-off qua router (channel trùng NOTE_ON — track.id). -var bStopTracks=activeTracksRef.current||[];bStopTracks.forEach(function(bst){try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:bst.id,pitch:pitch,velocity:0,percussion:!!(bst.synth_engine&&bst.synth_engine.soundfont_bank===128)});}catch(e){}});}else if(window.SonicSF&&window.SonicSF.stopNote){var stopTracks=activeTracksRef.current||[];stopTracks.forEach(function(st){// Only stop channels that actually carry this track's notes — +var bStopTracks=activeTracksRef.current||[];bStopTracks.forEach(function(bst){try{if(heldMidiNotesRef.current[bst.id]!==undefined){heldMidiNotesRef.current[bst.id]=Math.max(0,heldMidiNotesRef.current[bst.id]-1);if(heldMidiNotesRef.current[bst.id]===0){delete heldMidiNotesRef.current[bst.id];}}}catch(err){}try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:bst.id,pitch:pitch,velocity:0,percussion:!!(bst.synth_engine&&bst.synth_engine.soundfont_bank===128)});}catch(e){}});}else if(window.SonicSF&&window.SonicSF.stopNote){var stopTracks=activeTracksRef.current||[];stopTracks.forEach(function(st){// Only stop channels that actually carry this track's notes — // an index-based fallback could hit another track's dedicated // channel and kill its sound. if(!st.synth_engine&&st.midiChannel===undefined)return;var stCh=assignTrackMidiChannel(st,stopTracks);if(isStandaloneSf())stopNativeSfNote('midi_'+st.id+'_'+pitch);window.SonicSF.stopNote(stCh,pitch);// MIDI Keyboard → Carla bridge: note-off khi thả phím — tránh @@ -1315,7 +1315,7 @@ const fxEnabled=track.fxActive!==false;const chainMods=fxEnabled?fxChain.filter( // fully separate from the audio-clips chain — so the A (audio) bypass // NEVER affects the soundfont instrument, and the ♪ bypass never touches // audio clips/sections. -let sfEntry=null,sfOut=null,sfPan=null,sfAnalyser=null,sfRouteGain=null,sfDryGain=null,sfMods=[];if(track.midiItems&&track.midiItems.length>0){sfEntry=context.createGain();sfOut=context.createGain();sfPan=context.createStereoPanner();sfPan.pan.setValueAtTime((track.pan??0)/100,context.currentTime);sfAnalyser=context.createAnalyser();sfAnalyser.fftSize=2048;// NOTE: sfEntry is NOT fed from gainNode — the FluidSynth output is +let sfEntry=null,sfOut=null,sfPan=null,sfAnalyser=null,sfRouteGain=null,sfDryGain=null,sfMods=[];const isMidiTrack=track.midiItems&&track.midiItems.length>0||track.type==='MIDI'||!!track.instrumentId;if(isMidiTrack){sfEntry=context.createGain();sfOut=context.createGain();sfPan=context.createStereoPanner();sfPan.pan.setValueAtTime((track.pan??0)/100,context.currentTime);sfAnalyser=context.createAnalyser();sfAnalyser.fftSize=2048;// NOTE: sfEntry is NOT fed from gainNode — the FluidSynth output is // routed into sfEntry directly by updateSfRouting(). Connecting // gainNode → sfEntry here would LEAK every audio clip/section into the // SF chain → masterBus.input → mastering even when bypassed (double @@ -1368,7 +1368,7 @@ if(_prNode.sfRouteGain&&_prNode.sfDryGain){const _prTrk=list.find(t=>t.id===_act // bỏ qua → node giữ bypass cũ (tạo khi mastering OFF / audioBypass // true) → preview âm soundfont KHÔNG qua mastering cho tới khi bật/ // tắt power (effect re-sync). MIDI preview route theo ♪ (midiBypass). -if(_prNode.route&&_prNode.route.routeGain&&_prNode.route.dryGain){const _prTrk2=list.find(t=>t.id===_activeSub.trackId);const _b2=effMidiBypass(_prTrk2||{id:_activeSub.trackId});_prNode.route.routeGain.gain.value=_b2?0:1;_prNode.route.dryGain.gain.value=_b2?1:0;}return;}}}const midiAudible=list.filter(t=>t.midiItems&&t.midiItems.length>0&&computeTrackAudibleGain(list,t)>0);console.log('[SFRoute] activeTab='+_activeTabId,'midiAudible='+midiAudible.map(t=>t.id+'(midi'+(t.midiItems||[]).length+')').join(',')||'none');if(midiAudible.length===1){const t=midiAudible[0];// SECTION-TAB: track id của section clone TRÙNG main track id → node +if(_prNode.route&&_prNode.route.routeGain&&_prNode.route.dryGain){const _prTrk2=list.find(t=>t.id===_activeSub.trackId);const _b2=effMidiBypass(_prTrk2||{id:_activeSub.trackId});_prNode.route.routeGain.gain.value=_b2?0:1;_prNode.route.dryGain.gain.value=_b2?1:0;}return;}}}const midiAudible=list.filter(t=>(t.midiItems&&t.midiItems.length>0||t.isArmed||t.type==='MIDI'||!!t.instrumentId)&&computeTrackAudibleGain(list,t)>0);console.log('[SFRoute] activeTab='+_activeTabId,'midiAudible='+midiAudible.map(t=>t.id+'(midi'+(t.midiItems||[]).length+')').join(',')||'none');if(midiAudible.length===1){const t=midiAudible[0];// SECTION-TAB: track id của section clone TRÙNG main track id → node // chính (key = id) là node MAIN (route theo ♪ main — SAI context, MIDI // section bị kéo vào dry). ƯU TIÊN SUB-NODE (key _sub_ — // route theo ♪ section + thẳng masterBus.input) — không có sub-node → @@ -1386,7 +1386,7 @@ const _b=effMidiBypass(t);node.sfRouteGain.gain.value=_b?0:1;node.sfDryGain.gain // mastering cho tới khi bật/tắt power. MIDI preview theo ♪. if(node.route&&node.route.routeGain&&node.route.dryGain){const _b3=effMidiBypass(t);node.route.routeGain.gain.value=_b3?0:1;node.route.dryGain.gain.value=_b3?1:0;}return;}}// D5: bridge active + không track nào audible → ngắt khỏi graph (về // masterBus qua updateSfRouting lần sau khi có track). -if(window.AudioRoutingEngine&&window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){window.AudioRoutingEngine.disconnect();}else if(window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(null);}}catch(e){console.warn('updateSfRouting error:',e);}};const updateSfRoutingRef=useRef(null);// ── Đồng bộ mastering + routing NGAY TRƯỚC KHI PREVIEW (keybed/draw/MIDI file) ── +if(window.AudioRoutingEngine&&window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()&&window.BridgeAudioNode){window.AudioRoutingEngine.connect(window.BridgeAudioNode,null,null);}else if(window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(null);}}catch(e){console.warn('updateSfRouting error:',e);}};const updateSfRoutingRef=useRef(null);// ── Đồng bộ mastering + routing NGAY TRƯỚC KHI PREVIEW (keybed/draw/MIDI file) ── // Bug: preview instrument soundfont với MIDI keyboard KHÔNG tự động đi qua // mastering FX của main out — phải bật/tắt nút power mới có tác dụng. Nguyên // nhân: routing SF (setOutputDestination + sfRouteGain/sfDryGain) và graph diff --git a/app/static/js/services/aiGateway.js b/app/static/js/services/aiGateway.js index 1f0fe5d..a113919 100644 --- a/app/static/js/services/aiGateway.js +++ b/app/static/js/services/aiGateway.js @@ -294,7 +294,7 @@ ${rules.join('\n')}` }, async function callLLM({ provider, model, apiKey, baseUrl, messages, tools, toolChoice }) { const base = baseUrl.replace(/\/$/, ''); const url = `${base}/chat/completions`; - const origin = window.location.origin; + const origin = (window.API_BASE_URL || window.location.origin).replace(/\/+$/, ''); const urlOrigin = parseOrigin(url); const appOrigin = parseOrigin(origin); const sameOrigin = urlOrigin === appOrigin; @@ -341,7 +341,21 @@ ${rules.join('\n')}` }, throw new Error(detail); } - return await response.json(); + const ct = (response.headers.get('content-type') || '').toLowerCase(); + if (!ct.includes('application/json') && !ct.includes('text/json')) { + const raw = (await response.text()).slice(0, 300); + throw new Error(`Phản hồi không phải JSON (HTTP ${response.status}, ${ct || 'no content-type'}). +Có thể proxy trỏ sai địa chỉ hoặc server trả trang HTML. +Raw: ${raw}`); + } + + try { + return await response.json(); + } catch (e) { + const raw = (await response.text()).slice(0, 300); + throw new Error(`JSON parse lỗi từ server AI (HTTP ${response.status}): ${e.message} +Raw: ${raw}`); + } } function extractFunctionCalls(completion) { diff --git a/app/static/js/services/nativeBridgeService.js b/app/static/js/services/nativeBridgeService.js index 6c0a0af..d7aec79 100644 --- a/app/static/js/services/nativeBridgeService.js +++ b/app/static/js/services/nativeBridgeService.js @@ -135,10 +135,13 @@ /** * 3. OPEN FLOATING CHILD WINDOW NATIVE GUI */ - openNativeGUI: async function (pluginId) { + openNativeGUI: async function (pluginId, channel) { if (!this._tauri()) return false; try { - await window.__TAURI__.core.invoke('open_vst_gui', { pluginId: pluginId }); + await window.__TAURI__.core.invoke('open_vst_gui', { + pluginId: pluginId, + channel: channel === undefined ? 0 : channel + }); return true; } catch (e) { console.warn('[BridgeService] open_vst_gui:', e); diff --git a/native_bridge/src/NativeInstrumentEngine.cpp b/native_bridge/src/NativeInstrumentEngine.cpp index fea6110..dc13a96 100644 --- a/native_bridge/src/NativeInstrumentEngine.cpp +++ b/native_bridge/src/NativeInstrumentEngine.cpp @@ -47,8 +47,7 @@ bool FluidSynthInstrument::init(double sampleRate, uint32_t maxBlockSize) { void FluidSynthInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) { if (!synth) return; - fluid_synth_bank_select(FS_SYNTH, channel, bank); - fluid_synth_program_change(FS_SYNTH, channel, program); + fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank, program); } void FluidSynthInstrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) { @@ -69,7 +68,7 @@ void FluidSynthInstrument::controlChange(uint32_t channel, uint32_t cc, uint32_t void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) { if (!synth) return; - fluid_synth_program_change(FS_SYNTH, channel, program); + fluid_synth_program_select(FS_SYNTH, channel, sfontId, 0, program); } void FluidSynthInstrument::pitchBend(uint32_t channel, uint32_t bend14) { diff --git a/native_bridge/src/Vst3Instrument.cpp b/native_bridge/src/Vst3Instrument.cpp index 7fd071f..fa625f8 100644 --- a/native_bridge/src/Vst3Instrument.cpp +++ b/native_bridge/src/Vst3Instrument.cpp @@ -1,4 +1,4 @@ -// native_bridge/src/Vst3Instrument.cpp +// native_bridge/src/Vst3Instrument.cpp // VST3 host via the Steinberg VST3 SDK (submodule vst3sdk/). // // Without the SDK (HAVE_VST3SDK undefined — vst3sdk submodule missing) every @@ -10,6 +10,9 @@ #include "Vst3Instrument.h" #ifdef HAVE_VST3SDK +#ifdef _WIN32 +#include +#endif #include "public.sdk/source/vst/hosting/module.h" #include "public.sdk/source/vst/hosting/hostclasses.h" #include "public.sdk/source/vst/hosting/processdata.h" @@ -75,6 +78,7 @@ using Steinberg::Vst::IParamValueQueue; using Steinberg::Vst::kNoParamId; using Steinberg::Vst::BusInfo; using Steinberg::Vst::kAudio; +using Steinberg::Vst::kEvent; using Steinberg::Vst::kInput; using Steinberg::Vst::kOutput; using Steinberg::Vst::kRealtime; @@ -287,6 +291,26 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) { ctrlCP->connect(compCP); } + // Activate all audio and event buses (MIDI & Audio) - required by VST3 specification. + { + int32 numAudioInputs = component->getBusCount(kAudio, kInput); + for (int32 i = 0; i < numAudioInputs; ++i) { + component->activateBus(kAudio, kInput, i, true); + } + int32 numAudioOutputs = component->getBusCount(kAudio, kOutput); + for (int32 i = 0; i < numAudioOutputs; ++i) { + component->activateBus(kAudio, kOutput, i, true); + } + int32 numEventInputs = component->getBusCount(kEvent, kInput); + for (int32 i = 0; i < numEventInputs; ++i) { + component->activateBus(kEvent, kInput, i, true); + } + int32 numEventOutputs = component->getBusCount(kEvent, kOutput); + for (int32 i = 0; i < numEventOutputs; ++i) { + component->activateBus(kEvent, kOutput, i, true); + } + } + // Audio processor: setup → activate → start processing (audioclient.cpp). FUnknownPtr processor(component); if (!processor) { @@ -372,7 +396,7 @@ void Vst3Instrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, ui e.ppqPosition = 0; e.flags = Event::kIsLive; e.type = Event::kNoteOnEvent; - e.noteOn.channel = (int16)channel; + e.noteOn.channel = 0; // Force to MIDI Channel 1 (0) for VSTi compatibility e.noteOn.pitch = (int16)pitch; e.noteOn.tuning = 0.f; e.noteOn.velocity = velocity; @@ -395,7 +419,7 @@ void Vst3Instrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOf e.ppqPosition = 0; e.flags = Event::kIsLive; e.type = Event::kNoteOffEvent; - e.noteOff.channel = (int16)channel; + e.noteOff.channel = 0; // Force to MIDI Channel 1 (0) for VSTi compatibility e.noteOff.pitch = (int16)pitch; e.noteOff.velocity = 0.f; e.noteOff.noteId = -1; @@ -411,8 +435,8 @@ void Vst3Instrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value #else auto* s = static_cast(state_); if (!s || !s->controller) return; - ParamID tag = midiControllerTag(s->controller, (int16)channel, (int16)cc, - kHostMidiCC | (ParamID)(cc & 0x7F)); + ParamID tag = midiControllerTag(s->controller, 0, (int16)cc, + kHostMidiCC); ParamValue v = value / 127.0; // setParamNormalized covers single-component plugins; the param queue // reaches split plugins that read inputParameterChanges inside process(). @@ -429,7 +453,7 @@ void Vst3Instrument::programChange(uint32_t channel, uint32_t program) { #else auto* s = static_cast(state_); if (!s || !s->controller) return; - ParamID tag = kHostMidiProgramChange | (ParamID)(channel & 0xF); + ParamID tag = kHostMidiProgramChange; // ponytail: normalized value should be program/(count-1) from the program // list param stepCount; 127ths is a reasonable approximation. ParamValue v = (program & 0x7F) / 127.0; @@ -446,8 +470,8 @@ void Vst3Instrument::pitchBend(uint32_t channel, uint32_t bend14) { #else auto* s = static_cast(state_); if (!s || !s->controller) return; - ParamID tag = midiControllerTag(s->controller, (int16)channel, (int16)kPitchBend, - kHostMidiPitchBend | (ParamID)(channel & 0xF)); + ParamID tag = midiControllerTag(s->controller, 0, (int16)kPitchBend, + kHostMidiPitchBend); ParamValue v = bend14 / 16383.0; s->controller->setParamNormalized(tag, v); int32 idx = 0; @@ -498,10 +522,21 @@ bool Vst3Instrument::openGUI(void* parentWindowHandle) { view->setFrame(&s->plugFrame); tresult ts = view->isPlatformTypeSupported(kPlatformTypeHWND); std::cerr << "[dbg] openGUI: isPlatformTypeSupported=" << (int)ts << std::endl; - if (ts != kResultTrue && ts != kResultOk) return false; + // Log only — proceed to attach anyway to support non-compliant plugins. tresult ta = view->attached(parentWindowHandle, kPlatformTypeHWND); std::cerr << "[dbg] openGUI: attached=" << (int)ta << std::endl; if (ta != kResultOk) return false; +#ifdef _WIN32 + HWND hwnd = (HWND)parentWindowHandle; + Steinberg::ViewRect rect; + if (view->getSize(&rect) == kResultOk) { + int w = rect.right - rect.left; + int h = rect.bottom - rect.top; + RECT r = { 0, 0, w, h }; + AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE); + SetWindowPos(hwnd, nullptr, 0, 0, r.right - r.left, r.bottom - r.top, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); + } +#endif s->view = view; guiAttached_ = true; // ponytail: the bridge loop is a worker thread without a Windows message @@ -556,7 +591,7 @@ void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t } tresult pr = processor->process(s->processData); float mx = 0.f; - if (pr == kResultOk && s->processData.numOutputs > 0) { + if (pr >= 0 && s->processData.numOutputs > 0) { const Steinberg::Vst::AudioBusBuffers& out = s->processData.outputs[0]; const float* buf0 = out.numChannels > 0 ? out.channelBuffers32[0] : nullptr; const float* buf1 = out.numChannels > 1 ? out.channelBuffers32[1] : nullptr; diff --git a/native_bridge/src/main.cpp b/native_bridge/src/main.cpp index 4924009..43f4d9d 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -14,9 +14,9 @@ #include #include #include -#include #endif +#include #include #include #include @@ -48,6 +48,19 @@ static void sleep_ms(uint32_t ms) { #endif } +#ifdef _WIN32 +static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + if (uMsg == WM_DESTROY) { + void* ptr = (void*)GetWindowLongPtrA(hwnd, GWLP_USERDATA); + if (ptr) { + INativeInstrument* inst = static_cast(ptr); + inst->closeGUI(); + } + } + return DefWindowProcA(hwnd, uMsg, wParam, lParam); +} +#endif + // B9: native Win32 window for the VST editor (replaces the WebView2 surface — // the HTML window was drawn ON TOP of the plugin GUI). MUST be created on the // ChannelWorker thread so the worker's idle message pump services its messages. @@ -57,7 +70,7 @@ static void* create_native_vst_window(const char* title) { static bool registered = false; if (!registered) { WNDCLASSA wc = {}; - wc.lpfnWndProc = DefWindowProcA; + wc.lpfnWndProc = VstWindowProc; wc.hInstance = GetModuleHandleA(nullptr); wc.lpszClassName = kWndClass; RegisterClassA(&wc); @@ -86,7 +99,7 @@ public: ChannelWorker() { th_ = std::thread([this] { #ifdef _WIN32 - CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + OleInitialize(nullptr); #endif std::unique_lock lk(mu_); for (;;) { @@ -112,7 +125,7 @@ public: lk.lock(); } #ifdef _WIN32 - CoUninitialize(); + OleUninitialize(); #endif }); } @@ -229,6 +242,10 @@ int main(int argc, char* argv[]) { instruments.renderAll(shmIPC->masterLeft + from, shmIPC->masterRight + from, to - from); }; + double blockDurationMs = (double)block / sampleRate * 1000.0; + auto startTime = std::chrono::steady_clock::now(); + uint64_t blockCount = 0; + // 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP while (true) { #ifdef _WIN32 @@ -289,8 +306,6 @@ int main(int argc, char* argv[]) { playheadSamples = c.arg1; } } else if (c.type == 4) { // OPEN_GUI (A7): arg1 = parent HWND (0 → bridge tự tạo native window), arg2 = plugin id - // ponytail: per-plugin channel mapping chua co — gan GUI cho - // instrument dau tien duoc load (smoke test = 1 instrument). // Chay tren CUNG ChannelWorker da load instrument: thread rieng // cho openGUI lai tao COM apartment moi, con plugin thi song o // apartment cu da chet (load thread exit) -> Nexus attached() @@ -299,16 +314,14 @@ int main(int argc, char* argv[]) { // len worker (async, VST3 init co the mat giay) truoc khi type=4 // duoc xu ly — khong doi, instruments.get() con rong -> "no // instrument loaded". Poll toi da 10s cho LOAD hoan tat. - uint32_t guiCh = 16; - for (int tries = 0; tries < 200 && guiCh == 16; ++tries) { - for (uint32_t ch = 0; ch < 16; ++ch) { - if (instruments.get(ch)) { guiCh = ch; break; } - } - if (guiCh == 16) sleep_ms(50); + uint32_t guiCh = c.channel; + if (guiCh >= 16) guiCh = 0; + for (int tries = 0; tries < 200 && !instruments.get(guiCh); ++tries) { + sleep_ms(50); } - if (guiCh == 16) { + if (!instruments.get(guiCh)) { std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << c.arg1 - << " plugin=" << c.arg2 << " (no instrument loaded)" << std::endl; + << " plugin=" << c.arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl; } else { if (!workers[guiCh]) workers[guiCh] = std::make_unique(); workers[guiCh]->post([&instruments, &guiWindows, guiCh, arg1 = c.arg1, arg2 = std::string(c.arg2)]() { @@ -317,14 +330,27 @@ int main(int argc, char* argv[]) { void* hwnd = (void*)(uintptr_t)arg1; #ifdef _WIN32 if (arg1 == 0) { - // B9: bridge tự tạo native window — editor VST3 đính - // vào đây; message pump bởi ChannelWorker idle loop. - hwnd = create_native_vst_window(arg2.c_str()); - if (!hwnd) { - std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl; - return; + HWND existingHwnd = nullptr; + auto it = guiWindows.find(guiCh); + if (it != guiWindows.end()) { + existingHwnd = (HWND)it->second; + } + if (existingHwnd && IsWindow(existingHwnd)) { + hwnd = existingHwnd; + SetWindowTextA((HWND)hwnd, arg2.c_str()); + ShowWindow((HWND)hwnd, SW_SHOW); + SetForegroundWindow((HWND)hwnd); + } else { + hwnd = create_native_vst_window(arg2.c_str()); + if (!hwnd) { + std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl; + return; + } + guiWindows[guiCh] = hwnd; // keep window alive + if (auto* inst = instruments.get(guiCh)) { + SetWindowLongPtrA((HWND)hwnd, GWLP_USERDATA, (LONG_PTR)inst); + } } - guiWindows[guiCh] = hwnd; // keep window alive } #else (void)guiWindows; @@ -376,8 +402,19 @@ int main(int argc, char* argv[]) { break; } - // E. Sleep briefly for the next frame tick (block@44100 ≈ 5.8ms) - sleep_ms(1); + // E. Synchronize with real-time audio playback + blockCount++; + auto targetTime = startTime + std::chrono::microseconds(static_cast(blockCount * blockDurationMs * 1000.0)); + auto now = std::chrono::steady_clock::now(); + if (now < targetTime) { + auto diff = std::chrono::duration_cast(targetTime - now).count(); + if (diff > 1000) { + sleep_ms(diff / 1000); + } + while (std::chrono::steady_clock::now() < targetTime) { + std::this_thread::yield(); + } + } } #ifdef _WIN32 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2768696..eae816b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -22,6 +22,7 @@ use shm::{Shm, ShmState}; struct EngineProcess(Mutex>); struct BridgeProcess(Mutex>); +struct BridgeSampleRate(Mutex); /// Kill the currently managed bridge child (if any) — a restart MUST never /// leave the old bridge running, else two daw_vst_bridge.exe race on the same @@ -129,12 +130,17 @@ fn spawn_bridge( } match found { Some((bridge_exe, label)) => { + let sample_rate = { + let rate_state = app.state::(); + let g = rate_state.0.lock().unwrap_or_else(|p| p.into_inner()); + *g + }; match app .shell() .command(&bridge_exe) .env("SF_SHM_NAME", shm::SHM_NAME) .env("SF_PARENT_PID", std::process::id().to_string()) - .env("SF_SAMPLE_RATE", "44100") // B8: JS resampler handles mismatches (C5) + .env("SF_SAMPLE_RATE", sample_rate.to_string()) .env("SF_BLOCK_SIZE", "256") .spawn() { @@ -214,7 +220,8 @@ pub fn run() { load_native_instrument, open_vst_gui, bridge_status, - transport_control + transport_control, + restart_bridge_with_sample_rate ]) .setup(|app| { let res_dir = app @@ -308,6 +315,7 @@ pub fn run() { if shm_created.is_some() { "created" } else { "unavailable (non-windows or error)" } ); app.manage(ShmState(Mutex::new(shm_created))); + app.manage(BridgeSampleRate(Mutex::new(48000))); // Append bridge diagnostics to spawn.log and spawn the sidecar (B3). let app_handle = app.handle().clone(); @@ -387,7 +395,7 @@ pub fn run() { } } drop(guard); - std::thread::sleep(std::time::Duration::from_millis(5)); + std::thread::sleep(std::time::Duration::from_millis(1)); } }); @@ -509,7 +517,7 @@ fn load_native_instrument(app: AppHandle, path: String, instrument_type: u8, cha } #[tauri::command] -fn open_vst_gui(app: AppHandle, plugin_id: String) -> Result<(), String> { +fn open_vst_gui(app: AppHandle, plugin_id: String, channel: u8) -> Result<(), String> { // B9: bridge tự tạo native Win32 window cho editor VST3 (không qua // WebView2 — HTML window cũ vẽ ĐÈ lên GUI plugin). push_control(4,0,0,0) // với hwnd=0 báo bridge tạo window riêng trên ChannelWorker thread. @@ -518,8 +526,8 @@ fn open_vst_gui(app: AppHandle, plugin_id: String) -> Result<(), String> { match lock { Ok(guard) => match guard.as_ref() { Some(shm) => { - let ok = shm.push_control(4, 0, 0, 0, &plugin_id); - eprintln!("open_vst_gui: push_control type=4 hwnd=0 (bridge-native) ok={}", ok); + let ok = shm.push_control(4, 0, 0, channel as u32, &plugin_id); + eprintln!("open_vst_gui: push_control type=4 hwnd=0 ch={} ok={}", channel, ok); } None => eprintln!("open_vst_gui: bridge shm unavailable"), }, @@ -557,7 +565,11 @@ fn bridge_status(app: AppHandle) -> Result { write_index: shm.write_index(), block_timestamp: shm.block_timestamp(), shm_size_bytes: std::mem::size_of::(), - sample_rate: 44100, + sample_rate: { + let rate_state = app.state::(); + let g = rate_state.0.lock().unwrap_or_else(|p| p.into_inner()); + *g + }, block_size: shm::AUDIO_BLOCK_SIZE as u32, }, None => BridgeStatus { @@ -596,3 +608,33 @@ fn transport_control(app: AppHandle, kind: String, playhead: Option) -> Res .then_some(()) .ok_or_else(|| "control queue full".to_string()) } + +#[tauri::command] +fn restart_bridge_with_sample_rate(app: AppHandle, sample_rate: u32) -> Result<(), String> { + println!("restart_bridge_with_sample_rate: {}", sample_rate); + { + let rate_state = app.state::(); + { + let mut g = rate_state.0.lock().unwrap_or_else(|p| p.into_inner()); + *g = sample_rate; + } + } + kill_bridge(&app); + let res_dir = app.path().resource_dir().unwrap_or_default(); + let exe_dir = std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(|d| d.to_path_buf())) + .unwrap_or_default(); + let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into()); + let spawn_log_path = std::path::Path::new(&log_dir) + .join("SonicForgeDAW") + .join("logs") + .join("spawn.log"); + let mut log_line = format!("[tauri] restarting bridge with sample rate {}\n", sample_rate); + let ok = spawn_bridge(&app, &res_dir, &exe_dir, &mut log_line, &spawn_log_path); + if ok { + Ok(()) + } else { + Err("Failed to spawn bridge with new sample rate".into()) + } +}