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
This commit is contained in:
2026-08-13 11:26:39 +07:00
parent 49549aba2e
commit d168328004
8 changed files with 218 additions and 59 deletions
+34 -5
View File
@@ -1416,6 +1416,20 @@ function getAudioContext() {
window.SonicMidiRouter.setBridgeConnected(connected); window.SonicMidiRouter.setBridgeConnected(connected);
console.log('[Bridge] bootstrap connected=', connected); console.log('[Bridge] bootstrap connected=', connected);
if (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.BridgeAudioNode.init(audioCtx);
window.NativeBridgeService.onAudio(function (l, r) { window.BridgeAudioNode.onAudio(l, r); }); window.NativeBridgeService.onAudio(function (l, r) { window.BridgeAudioNode.onAudio(l, r); });
if (window.AudioRoutingEngine) window.AudioRoutingEngine.connect(window.BridgeAudioNode, null, null); if (window.AudioRoutingEngine) window.AudioRoutingEngine.connect(window.BridgeAudioNode, null, null);
@@ -15378,7 +15392,7 @@ const App = () => {
// Requirement 2: chn VST3 qua nút Synth load xong m native GUI // Requirement 2: chn VST3 qua nút Synth load xong m native GUI
// (C++ attach editor vào ca s bridge t to control type=4, hwnd=0). // (C++ attach editor vào ca s bridge t to control type=4, hwnd=0).
if (ok && btype === 'VST3' && window.NativeBridgeService.openNativeGUI) { 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 || []; var bTracks = activeTracksRef.current || [];
bTracks.forEach(function (bt) { bTracks.forEach(function (bt) {
if (!bt.isArmed) return; 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) {} 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) { } else if (window.SonicSF) {
@@ -15722,6 +15742,14 @@ const App = () => {
// D1: bridge note-off qua router (channel trùng NOTE_ON track.id). // D1: bridge note-off qua router (channel trùng NOTE_ON track.id).
var bStopTracks = activeTracksRef.current || []; var bStopTracks = activeTracksRef.current || [];
bStopTracks.forEach(function (bst) { 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) {} 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) { } else if (window.SonicSF && window.SonicSF.stopNote) {
@@ -21216,7 +21244,8 @@ const App = () => {
// NEVER affects the soundfont instrument, and the bypass never touches // NEVER affects the soundfont instrument, and the bypass never touches
// audio clips/sections. // audio clips/sections.
let sfEntry = null, sfOut = null, sfPan = null, sfAnalyser = null, sfRouteGain = null, sfDryGain = null, sfMods = []; 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(); sfEntry = context.createGain();
sfOut = context.createGain(); sfOut = context.createGain();
sfPan = context.createStereoPanner(); 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'); console.log('[SFRoute] activeTab=' + _activeTabId, 'midiAudible=' + midiAudible.map(t => t.id + '(midi' + (t.midiItems || []).length + ')').join(',') || 'none');
if (midiAudible.length === 1) { if (midiAudible.length === 1) {
const t = midiAudible[0]; const t = midiAudible[0];
@@ -21454,8 +21483,8 @@ const App = () => {
} }
// D5: bridge active + không track nào audible ngt khi graph (v // D5: bridge active + không track nào audible ngt khi graph (v
// masterBus qua updateSfRouting ln sau khi có track). // masterBus qua updateSfRouting ln sau khi có track).
if (window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) { if (window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive() && window.BridgeAudioNode) {
window.AudioRoutingEngine.disconnect(); window.AudioRoutingEngine.connect(window.BridgeAudioNode, null, null);
} else if (window.SonicSF && window.SonicSF.setOutputDestination) { } else if (window.SonicSF && window.SonicSF.setOutputDestination) {
window.SonicSF.setOutputDestination(null); window.SonicSF.setOutputDestination(null);
} }
+7 -7
View File
@@ -247,7 +247,7 @@ try{masterBus.inputAnalyser.disconnect();masterBus.maximizerCompressor.disconnec
// ("BiquadFilterNode: state is bad") and can leave the master routing broken // ("BiquadFilterNode: state is bad") and can leave the master routing broken
// → global silence. The React effect applies it once per settings change. // → 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.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<minDistance){minDistance=dist;bestSample=i;}}}return bestSample/sampleRate;};class ClientMIDIRecorder{constructor(audioContext,bpm=120,timeSigNumerator=4){this.audioCtx=audioContext;this.bpm=bpm;this.timeSigNum=timeSigNumerator;this.isRecording=false;this.tempMidiItemId=null;this.activeNotes=new Map();// Store pitch -> { 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<minDistance){minDistance=dist;bestSample=i;}}}return bestSample/sampleRate;};class ClientMIDIRecorder{constructor(audioContext,bpm=120,timeSigNumerator=4){this.audioCtx=audioContext;this.bpm=bpm;this.timeSigNum=timeSigNumerator;this.isRecording=false;this.tempMidiItemId=null;this.activeNotes=new Map();// Store pitch -> { noteId, startBeat, velocity }
this.recordedNotes=[];this.recStartAudioTime=0.0;this.recStartBar=0.0;this.selectedMidiInputId=null;// Compute round-trip browser latency 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 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 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. // 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 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). // (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<mt.length;ci++){if(mt[ci].id===trackId){curTrk=mt[ci];break;}}// ── Unload Carla bridge khi chuyển từ VSTi sang instrument KHÔNG phải VST ── if(ok&&btype==='VST3'&&window.NativeBridgeService.openNativeGUI){try{await window.NativeBridgeService.openNativeGUI(instrumentId,bch);}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<mt.length;ci++){if(mt[ci].id===trackId){curTrk=mt[ci];break;}}// ── Unload Carla bridge khi chuyển từ VSTi sang instrument KHÔNG phải VST ──
// (soundfont/GM/default). Nếu không, Carla vẫn chạy với VSTi cũ → MIDI vẫn // (soundfont/GM/default). Nếu không, Carla vẫn chạy với VSTi cũ → MIDI vẫn
// play qua Carla bridge (âm sai instrument + âm kẹt không dừng được). // play qua Carla bridge (âm sai instrument + âm kẹt không dừng được).
try{const _hasInst=!!instrumentId;const _wasVst=curTrk&&curTrk.synth_engine&&String(curTrk.synth_engine.type||'').indexOf('vst')!==-1;const _nowVst=_hasInst&&!isSfInstrument;if(_wasVst&&!_nowVst&&window.SonicCarlaMidi&&window.SonicCarlaMidi.stopBridge){window.SonicCarlaMidi.stopBridge();try{if(window.SonicMidiRouter)window.SonicMidiRouter.panic();else if(window.SonicSF&&window.SonicSF.stopAll)window.SonicSF.stopAll();}catch(e){}stopAllNativeSfNotes();console.log('[Instrument] Carla bridge unloaded — track',trackId,'switched from VSTi to non-VST');}}catch(e){console.warn('[Instrument] carla-stop on instrument switch error:',e);}var mch=curTrk?assignTrackMidiChannel(curTrk,mt):sfBank===128?9:0;updateActiveTracks(prev=>prev.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 try{const _hasInst=!!instrumentId;const _wasVst=curTrk&&curTrk.synth_engine&&String(curTrk.synth_engine.type||'').indexOf('vst')!==-1;const _nowVst=_hasInst&&!isSfInstrument;if(_wasVst&&!_nowVst&&window.SonicCarlaMidi&&window.SonicCarlaMidi.stopBridge){window.SonicCarlaMidi.stopBridge();try{if(window.SonicMidiRouter)window.SonicMidiRouter.panic();else if(window.SonicSF&&window.SonicSF.stopAll)window.SonicSF.stopAll();}catch(e){}stopAllNativeSfNotes();console.log('[Instrument] Carla bridge unloaded — track',trackId,'switched from VSTi to non-VST');}}catch(e){console.warn('[Instrument] carla-stop on instrument switch error:',e);}var mch=curTrk?assignTrackMidiChannel(curTrk,mt):sfBank===128?9:0;updateActiveTracks(prev=>prev.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) // 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) → 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. // 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ữ 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) // (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): 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&&current.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 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&&current.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 // 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). 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 // an index-based fallback could hit another track's dedicated
// channel and kill its sound. // 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 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 // fully separate from the audio-clips chain — so the A (audio) bypass
// NEVER affects the soundfont instrument, and the ♪ bypass never touches // NEVER affects the soundfont instrument, and the ♪ bypass never touches
// audio clips/sections. // 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 // routed into sfEntry directly by updateSfRouting(). Connecting
// gainNode → sfEntry here would LEAK every audio clip/section into the // gainNode → sfEntry here would LEAK every audio clip/section into the
// SF chain → masterBus.input → mastering even when bypassed (double // 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 // 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/ // 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). // 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 // 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 <parent>_sub_<id> — // section bị kéo vào dry). ƯU TIÊN SUB-NODE (key <parent>_sub_<id> —
// route theo ♪ section + thẳng masterBus.input) — không có sub-node → // 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 ♪. // 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ề 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). // 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 // 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 // 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 // nhân: routing SF (setOutputDestination + sfRouteGain/sfDryGain) và graph
+15 -1
View File
@@ -294,7 +294,7 @@ ${rules.join('\n')}` },
async function callLLM({ provider, model, apiKey, baseUrl, messages, tools, toolChoice }) { async function callLLM({ provider, model, apiKey, baseUrl, messages, tools, toolChoice }) {
const base = baseUrl.replace(/\/$/, ''); const base = baseUrl.replace(/\/$/, '');
const url = `${base}/chat/completions`; 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 urlOrigin = parseOrigin(url);
const appOrigin = parseOrigin(origin); const appOrigin = parseOrigin(origin);
const sameOrigin = urlOrigin === appOrigin; const sameOrigin = urlOrigin === appOrigin;
@@ -341,7 +341,21 @@ ${rules.join('\n')}` },
throw new Error(detail); throw new Error(detail);
} }
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(); 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) { function extractFunctionCalls(completion) {
@@ -135,10 +135,13 @@
/** /**
* 3. OPEN FLOATING CHILD WINDOW NATIVE GUI * 3. OPEN FLOATING CHILD WINDOW NATIVE GUI
*/ */
openNativeGUI: async function (pluginId) { openNativeGUI: async function (pluginId, channel) {
if (!this._tauri()) return false; if (!this._tauri()) return false;
try { 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; return true;
} catch (e) { } catch (e) {
console.warn('[BridgeService] open_vst_gui:', e); console.warn('[BridgeService] open_vst_gui:', e);
+2 -3
View File
@@ -47,8 +47,7 @@ bool FluidSynthInstrument::init(double sampleRate, uint32_t maxBlockSize) {
void FluidSynthInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) { void FluidSynthInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) {
if (!synth) return; if (!synth) return;
fluid_synth_bank_select(FS_SYNTH, channel, bank); fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank, program);
fluid_synth_program_change(FS_SYNTH, channel, program);
} }
void FluidSynthInstrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) { 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) { void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) {
if (!synth) return; 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) { void FluidSynthInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
+45 -10
View File
@@ -1,4 +1,4 @@
// native_bridge/src/Vst3Instrument.cpp // native_bridge/src/Vst3Instrument.cpp
// VST3 host via the Steinberg VST3 SDK (submodule vst3sdk/). // VST3 host via the Steinberg VST3 SDK (submodule vst3sdk/).
// //
// Without the SDK (HAVE_VST3SDK undefined — vst3sdk submodule missing) every // Without the SDK (HAVE_VST3SDK undefined — vst3sdk submodule missing) every
@@ -10,6 +10,9 @@
#include "Vst3Instrument.h" #include "Vst3Instrument.h"
#ifdef HAVE_VST3SDK #ifdef HAVE_VST3SDK
#ifdef _WIN32
#include <windows.h>
#endif
#include "public.sdk/source/vst/hosting/module.h" #include "public.sdk/source/vst/hosting/module.h"
#include "public.sdk/source/vst/hosting/hostclasses.h" #include "public.sdk/source/vst/hosting/hostclasses.h"
#include "public.sdk/source/vst/hosting/processdata.h" #include "public.sdk/source/vst/hosting/processdata.h"
@@ -75,6 +78,7 @@ using Steinberg::Vst::IParamValueQueue;
using Steinberg::Vst::kNoParamId; using Steinberg::Vst::kNoParamId;
using Steinberg::Vst::BusInfo; using Steinberg::Vst::BusInfo;
using Steinberg::Vst::kAudio; using Steinberg::Vst::kAudio;
using Steinberg::Vst::kEvent;
using Steinberg::Vst::kInput; using Steinberg::Vst::kInput;
using Steinberg::Vst::kOutput; using Steinberg::Vst::kOutput;
using Steinberg::Vst::kRealtime; using Steinberg::Vst::kRealtime;
@@ -287,6 +291,26 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
ctrlCP->connect(compCP); 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). // Audio processor: setup → activate → start processing (audioclient.cpp).
FUnknownPtr<IAudioProcessor> processor(component); FUnknownPtr<IAudioProcessor> processor(component);
if (!processor) { if (!processor) {
@@ -372,7 +396,7 @@ void Vst3Instrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, ui
e.ppqPosition = 0; e.ppqPosition = 0;
e.flags = Event::kIsLive; e.flags = Event::kIsLive;
e.type = Event::kNoteOnEvent; 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.pitch = (int16)pitch;
e.noteOn.tuning = 0.f; e.noteOn.tuning = 0.f;
e.noteOn.velocity = velocity; e.noteOn.velocity = velocity;
@@ -395,7 +419,7 @@ void Vst3Instrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOf
e.ppqPosition = 0; e.ppqPosition = 0;
e.flags = Event::kIsLive; e.flags = Event::kIsLive;
e.type = Event::kNoteOffEvent; 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.pitch = (int16)pitch;
e.noteOff.velocity = 0.f; e.noteOff.velocity = 0.f;
e.noteOff.noteId = -1; e.noteOff.noteId = -1;
@@ -411,8 +435,8 @@ void Vst3Instrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value
#else #else
auto* s = static_cast<Vst3HostState*>(state_); auto* s = static_cast<Vst3HostState*>(state_);
if (!s || !s->controller) return; if (!s || !s->controller) return;
ParamID tag = midiControllerTag(s->controller, (int16)channel, (int16)cc, ParamID tag = midiControllerTag(s->controller, 0, (int16)cc,
kHostMidiCC | (ParamID)(cc & 0x7F)); kHostMidiCC);
ParamValue v = value / 127.0; ParamValue v = value / 127.0;
// setParamNormalized covers single-component plugins; the param queue // setParamNormalized covers single-component plugins; the param queue
// reaches split plugins that read inputParameterChanges inside process(). // reaches split plugins that read inputParameterChanges inside process().
@@ -429,7 +453,7 @@ void Vst3Instrument::programChange(uint32_t channel, uint32_t program) {
#else #else
auto* s = static_cast<Vst3HostState*>(state_); auto* s = static_cast<Vst3HostState*>(state_);
if (!s || !s->controller) return; 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 // ponytail: normalized value should be program/(count-1) from the program
// list param stepCount; 127ths is a reasonable approximation. // list param stepCount; 127ths is a reasonable approximation.
ParamValue v = (program & 0x7F) / 127.0; ParamValue v = (program & 0x7F) / 127.0;
@@ -446,8 +470,8 @@ void Vst3Instrument::pitchBend(uint32_t channel, uint32_t bend14) {
#else #else
auto* s = static_cast<Vst3HostState*>(state_); auto* s = static_cast<Vst3HostState*>(state_);
if (!s || !s->controller) return; if (!s || !s->controller) return;
ParamID tag = midiControllerTag(s->controller, (int16)channel, (int16)kPitchBend, ParamID tag = midiControllerTag(s->controller, 0, (int16)kPitchBend,
kHostMidiPitchBend | (ParamID)(channel & 0xF)); kHostMidiPitchBend);
ParamValue v = bend14 / 16383.0; ParamValue v = bend14 / 16383.0;
s->controller->setParamNormalized(tag, v); s->controller->setParamNormalized(tag, v);
int32 idx = 0; int32 idx = 0;
@@ -498,10 +522,21 @@ bool Vst3Instrument::openGUI(void* parentWindowHandle) {
view->setFrame(&s->plugFrame); view->setFrame(&s->plugFrame);
tresult ts = view->isPlatformTypeSupported(kPlatformTypeHWND); tresult ts = view->isPlatformTypeSupported(kPlatformTypeHWND);
std::cerr << "[dbg] openGUI: isPlatformTypeSupported=" << (int)ts << std::endl; 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); tresult ta = view->attached(parentWindowHandle, kPlatformTypeHWND);
std::cerr << "[dbg] openGUI: attached=" << (int)ta << std::endl; std::cerr << "[dbg] openGUI: attached=" << (int)ta << std::endl;
if (ta != kResultOk) return false; 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; s->view = view;
guiAttached_ = true; guiAttached_ = true;
// ponytail: the bridge loop is a worker thread without a Windows message // 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); tresult pr = processor->process(s->processData);
float mx = 0.f; 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 Steinberg::Vst::AudioBusBuffers& out = s->processData.outputs[0];
const float* buf0 = out.numChannels > 0 ? out.channelBuffers32[0] : nullptr; const float* buf0 = out.numChannels > 0 ? out.channelBuffers32[0] : nullptr;
const float* buf1 = out.numChannels > 1 ? out.channelBuffers32[1] : nullptr; const float* buf1 = out.numChannels > 1 ? out.channelBuffers32[1] : nullptr;
+55 -18
View File
@@ -14,9 +14,9 @@
#include <unistd.h> #include <unistd.h>
#include <cstdlib> #include <cstdlib>
#include <thread> #include <thread>
#include <chrono>
#endif #endif
#include <chrono>
#include <algorithm> #include <algorithm>
#include <condition_variable> #include <condition_variable>
#include <cstring> #include <cstring>
@@ -48,6 +48,19 @@ static void sleep_ms(uint32_t ms) {
#endif #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<INativeInstrument*>(ptr);
inst->closeGUI();
}
}
return DefWindowProcA(hwnd, uMsg, wParam, lParam);
}
#endif
// B9: native Win32 window for the VST editor (replaces the WebView2 surface — // 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 // 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. // 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; static bool registered = false;
if (!registered) { if (!registered) {
WNDCLASSA wc = {}; WNDCLASSA wc = {};
wc.lpfnWndProc = DefWindowProcA; wc.lpfnWndProc = VstWindowProc;
wc.hInstance = GetModuleHandleA(nullptr); wc.hInstance = GetModuleHandleA(nullptr);
wc.lpszClassName = kWndClass; wc.lpszClassName = kWndClass;
RegisterClassA(&wc); RegisterClassA(&wc);
@@ -86,7 +99,7 @@ public:
ChannelWorker() { ChannelWorker() {
th_ = std::thread([this] { th_ = std::thread([this] {
#ifdef _WIN32 #ifdef _WIN32
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); OleInitialize(nullptr);
#endif #endif
std::unique_lock<std::mutex> lk(mu_); std::unique_lock<std::mutex> lk(mu_);
for (;;) { for (;;) {
@@ -112,7 +125,7 @@ public:
lk.lock(); lk.lock();
} }
#ifdef _WIN32 #ifdef _WIN32
CoUninitialize(); OleUninitialize();
#endif #endif
}); });
} }
@@ -229,6 +242,10 @@ int main(int argc, char* argv[]) {
instruments.renderAll(shmIPC->masterLeft + from, shmIPC->masterRight + from, to - from); 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 // 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP
while (true) { while (true) {
#ifdef _WIN32 #ifdef _WIN32
@@ -289,8 +306,6 @@ int main(int argc, char* argv[]) {
playheadSamples = c.arg1; playheadSamples = c.arg1;
} }
} else if (c.type == 4) { // OPEN_GUI (A7): arg1 = parent HWND (0 → bridge tự tạo native window), arg2 = plugin id } 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 // Chay tren CUNG ChannelWorker da load instrument: thread rieng
// cho openGUI lai tao COM apartment moi, con plugin thi song o // cho openGUI lai tao COM apartment moi, con plugin thi song o
// apartment cu da chet (load thread exit) -> Nexus attached() // 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 // len worker (async, VST3 init co the mat giay) truoc khi type=4
// duoc xu ly — khong doi, instruments.get() con rong -> "no // duoc xu ly — khong doi, instruments.get() con rong -> "no
// instrument loaded". Poll toi da 10s cho LOAD hoan tat. // instrument loaded". Poll toi da 10s cho LOAD hoan tat.
uint32_t guiCh = 16; uint32_t guiCh = c.channel;
for (int tries = 0; tries < 200 && guiCh == 16; ++tries) { if (guiCh >= 16) guiCh = 0;
for (uint32_t ch = 0; ch < 16; ++ch) { for (int tries = 0; tries < 200 && !instruments.get(guiCh); ++tries) {
if (instruments.get(ch)) { guiCh = ch; break; } sleep_ms(50);
} }
if (guiCh == 16) sleep_ms(50); if (!instruments.get(guiCh)) {
}
if (guiCh == 16) {
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << c.arg1 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 { } else {
if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>(); if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>();
workers[guiCh]->post([&instruments, &guiWindows, guiCh, arg1 = c.arg1, arg2 = std::string(c.arg2)]() { 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; void* hwnd = (void*)(uintptr_t)arg1;
#ifdef _WIN32 #ifdef _WIN32
if (arg1 == 0) { if (arg1 == 0) {
// B9: bridge tự tạo native window — editor VST3 đính HWND existingHwnd = nullptr;
// vào đây; message pump bởi ChannelWorker idle loop. 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()); hwnd = create_native_vst_window(arg2.c_str());
if (!hwnd) { if (!hwnd) {
std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl; std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl;
return; return;
} }
guiWindows[guiCh] = hwnd; // keep window alive guiWindows[guiCh] = hwnd; // keep window alive
if (auto* inst = instruments.get(guiCh)) {
SetWindowLongPtrA((HWND)hwnd, GWLP_USERDATA, (LONG_PTR)inst);
}
}
} }
#else #else
(void)guiWindows; (void)guiWindows;
@@ -376,8 +402,19 @@ int main(int argc, char* argv[]) {
break; break;
} }
// E. Sleep briefly for the next frame tick (block@44100 ≈ 5.8ms) // E. Synchronize with real-time audio playback
sleep_ms(1); blockCount++;
auto targetTime = startTime + std::chrono::microseconds(static_cast<int64_t>(blockCount * blockDurationMs * 1000.0));
auto now = std::chrono::steady_clock::now();
if (now < targetTime) {
auto diff = std::chrono::duration_cast<std::chrono::microseconds>(targetTime - now).count();
if (diff > 1000) {
sleep_ms(diff / 1000);
}
while (std::chrono::steady_clock::now() < targetTime) {
std::this_thread::yield();
}
}
} }
#ifdef _WIN32 #ifdef _WIN32
+49 -7
View File
@@ -22,6 +22,7 @@ use shm::{Shm, ShmState};
struct EngineProcess(Mutex<Option<CommandChild>>); struct EngineProcess(Mutex<Option<CommandChild>>);
struct BridgeProcess(Mutex<Option<CommandChild>>); struct BridgeProcess(Mutex<Option<CommandChild>>);
struct BridgeSampleRate(Mutex<u32>);
/// Kill the currently managed bridge child (if any) — a restart MUST never /// 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 /// leave the old bridge running, else two daw_vst_bridge.exe race on the same
@@ -129,12 +130,17 @@ fn spawn_bridge(
} }
match found { match found {
Some((bridge_exe, label)) => { Some((bridge_exe, label)) => {
let sample_rate = {
let rate_state = app.state::<BridgeSampleRate>();
let g = rate_state.0.lock().unwrap_or_else(|p| p.into_inner());
*g
};
match app match app
.shell() .shell()
.command(&bridge_exe) .command(&bridge_exe)
.env("SF_SHM_NAME", shm::SHM_NAME) .env("SF_SHM_NAME", shm::SHM_NAME)
.env("SF_PARENT_PID", std::process::id().to_string()) .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") .env("SF_BLOCK_SIZE", "256")
.spawn() .spawn()
{ {
@@ -214,7 +220,8 @@ pub fn run() {
load_native_instrument, load_native_instrument,
open_vst_gui, open_vst_gui,
bridge_status, bridge_status,
transport_control transport_control,
restart_bridge_with_sample_rate
]) ])
.setup(|app| { .setup(|app| {
let res_dir = app let res_dir = app
@@ -308,6 +315,7 @@ pub fn run() {
if shm_created.is_some() { "created" } else { "unavailable (non-windows or error)" } if shm_created.is_some() { "created" } else { "unavailable (non-windows or error)" }
); );
app.manage(ShmState(Mutex::new(shm_created))); app.manage(ShmState(Mutex::new(shm_created)));
app.manage(BridgeSampleRate(Mutex::new(48000)));
// Append bridge diagnostics to spawn.log and spawn the sidecar (B3). // Append bridge diagnostics to spawn.log and spawn the sidecar (B3).
let app_handle = app.handle().clone(); let app_handle = app.handle().clone();
@@ -387,7 +395,7 @@ pub fn run() {
} }
} }
drop(guard); 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] #[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 // 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) // 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. // 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 { match lock {
Ok(guard) => match guard.as_ref() { Ok(guard) => match guard.as_ref() {
Some(shm) => { Some(shm) => {
let ok = shm.push_control(4, 0, 0, 0, &plugin_id); let ok = shm.push_control(4, 0, 0, channel as u32, &plugin_id);
eprintln!("open_vst_gui: push_control type=4 hwnd=0 (bridge-native) ok={}", ok); eprintln!("open_vst_gui: push_control type=4 hwnd=0 ch={} ok={}", channel, ok);
} }
None => eprintln!("open_vst_gui: bridge shm unavailable"), None => eprintln!("open_vst_gui: bridge shm unavailable"),
}, },
@@ -557,7 +565,11 @@ fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
write_index: shm.write_index(), write_index: shm.write_index(),
block_timestamp: shm.block_timestamp(), block_timestamp: shm.block_timestamp(),
shm_size_bytes: std::mem::size_of::<shm::SharedAudioBufferIPC>(), shm_size_bytes: std::mem::size_of::<shm::SharedAudioBufferIPC>(),
sample_rate: 44100, sample_rate: {
let rate_state = app.state::<BridgeSampleRate>();
let g = rate_state.0.lock().unwrap_or_else(|p| p.into_inner());
*g
},
block_size: shm::AUDIO_BLOCK_SIZE as u32, block_size: shm::AUDIO_BLOCK_SIZE as u32,
}, },
None => BridgeStatus { None => BridgeStatus {
@@ -596,3 +608,33 @@ fn transport_control(app: AppHandle, kind: String, playhead: Option<u32>) -> Res
.then_some(()) .then_some(())
.ok_or_else(|| "control queue full".to_string()) .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::<BridgeSampleRate>();
{
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())
}
}