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:
+34
-5
@@ -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: chọn VST3 qua nút Synth → load xong mở native GUI
|
// 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) {
|
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 → ngắt khỏi graph (về
|
// 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()) {
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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&¤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
|
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
|
// 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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+60
-23
@@ -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 (guiCh == 16) {
|
if (!instruments.get(guiCh)) {
|
||||||
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);
|
||||||
hwnd = create_native_vst_window(arg2.c_str());
|
if (it != guiWindows.end()) {
|
||||||
if (!hwnd) {
|
existingHwnd = (HWND)it->second;
|
||||||
std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl;
|
}
|
||||||
return;
|
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
|
#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
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user