d8f8506077
- open_vst_gui: bo WebviewWindowBuilder/thread, chi push_control type=4 (hwnd=0 -> bridge tao window) - main.cpp: create_native_vst_window (class SonicForge_Native_VST3_Class, 800x600, khong TOPMOST), tao trong ChannelWorker job, map guiWindows, capture arg2 by value (fix dangling) - app.jsx: guard isBridgeActive() 8 cho -> bridge active thi moi note di router -> pushEvent -> bridge (truoc day SF2 cam vi HAS_PYFLUIDSYNTH=FALSE -> /soundfont-render 501; VST3 path cu dung nativeSf/Carla) - E2E: SF2 NOTE_ON qua dispatchMidiEvent -> SHM peak 0.029745; Nexus GUI native hwnd OK (license/preset) - docs: TASKS.md + TEST_NOTES.md ghi batch fix + ket qua; gitignore vendor/junk
178 lines
6.7 KiB
JavaScript
178 lines
6.7 KiB
JavaScript
// app/static/js/services/nativeBridgeService.js
|
|
// JS client for the C++ Native Host Bridge (daw_vst_bridge.exe).
|
|
// All IPC goes through Tauri commands (Rust writes the shared memory) —
|
|
// WebView2 JS cannot map Windows shared memory directly.
|
|
(function () {
|
|
// Must match native_bridge/include/INativeInstrument.h enum InstrumentType
|
|
var INSTRUMENT_TYPE = { VST3: 0, VST2: 1, SF2: 2, SF3: 2, SFZ: 3 };
|
|
|
|
var service = {
|
|
isBridgeConnected: false,
|
|
activeInstrumentType: 'VST3',
|
|
_audioCb: null,
|
|
_statusCb: null,
|
|
_tauri: function () { return window.__TAURI__; },
|
|
|
|
/** D10: UI subscribes to bridge connection changes. cb({connected}) or null to clear. */
|
|
onStatusChange: function (cb) { this._statusCb = cb; },
|
|
_notifyStatus: function (connected) {
|
|
this.isBridgeConnected = !!connected;
|
|
if (this._statusCb) this._statusCb({ connected: !!connected });
|
|
},
|
|
|
|
/** Fix C: re-establish bridge routing once audio frames arrive again
|
|
* after bridge-down. Same wiring as the bootstrap block in app.jsx
|
|
* (getAudioContext -> BridgeAudioNode.init -> AudioRoutingEngine.connect). */
|
|
_reconnectInFlight: false,
|
|
_reconnectIfNeeded: function () {
|
|
if (this._reconnectInFlight) return;
|
|
this._reconnectInFlight = true;
|
|
var self = this;
|
|
var finish = function () { self._reconnectInFlight = false; };
|
|
try {
|
|
var ctx = (typeof getAudioContext === 'function') ? getAudioContext() : (window.__sharedAudioCtx || null);
|
|
if (ctx && window.BridgeAudioNode && window.AudioRoutingEngine) {
|
|
window.BridgeAudioNode.init(ctx);
|
|
window.AudioRoutingEngine.connect(window.BridgeAudioNode, null, null);
|
|
}
|
|
if (window.SonicMidiRouter) window.SonicMidiRouter.setBridgeConnected(true);
|
|
this._notifyStatus(true);
|
|
console.warn('[BridgeService] bridge recovered — routing re-established.');
|
|
} catch (e) {
|
|
console.warn('[BridgeService] reconnect failed:', e);
|
|
}
|
|
finish();
|
|
},
|
|
|
|
_init: function () {
|
|
if (!this._tauri()) {
|
|
// Dev mode (Linux / plain browser): no Tauri -> log-only, app uses SonicSF.
|
|
console.log('[BridgeService] Dev mode: no __TAURI__, native bridge disabled.');
|
|
return;
|
|
}
|
|
var self = this;
|
|
try {
|
|
window.__TAURI__.event.listen('bridge-audio', function (e) {
|
|
// Bridge recovered after bridge-down (or never bootstrapped):
|
|
// audio frames flow again -> re-establish routing (Fix C).
|
|
if (!self.isBridgeConnected) self._reconnectIfNeeded();
|
|
if (self._audioCb) self._audioCb(e.payload.l, e.payload.r);
|
|
});
|
|
window.__TAURI__.event.listen('bridge-down', function () {
|
|
self._notifyStatus(false);
|
|
if (window.SonicMidiRouter) window.SonicMidiRouter.setBridgeConnected(false);
|
|
if (window.AudioRoutingEngine) window.AudioRoutingEngine.disconnect();
|
|
console.warn('[BridgeService] bridge-down event received.');
|
|
});
|
|
} catch (e) {
|
|
console.warn('[BridgeService] event listen failed:', e);
|
|
}
|
|
this.queryStatus();
|
|
},
|
|
|
|
queryStatus: async function () {
|
|
if (!this._tauri()) return { connected: false };
|
|
try {
|
|
var s = await window.__TAURI__.core.invoke('bridge_status');
|
|
this._notifyStatus(!!s.connected);
|
|
return s;
|
|
} catch (e) {
|
|
this._notifyStatus(false);
|
|
return { connected: false };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 1. LOAD NEW INSTRUMENT INTO BRIDGE (VST3 / VST2 / SF2 / SF3 / SFZ)
|
|
* instrumentType: 'VST3' | 'VST2' | 'SF2' | 'SF3' | 'SFZ'
|
|
* channel: MIDI channel to assign this instrument to (A10 multi-instance).
|
|
*/
|
|
loadInstrument: async function (filePath, instrumentType, channel) {
|
|
this.activeInstrumentType = instrumentType;
|
|
console.log('[BridgeService] Loading ' + instrumentType + ' asset: ' + filePath + ' ch=' + channel);
|
|
if (!this._tauri()) return false;
|
|
try {
|
|
await window.__TAURI__.core.invoke('load_native_instrument', {
|
|
path: filePath,
|
|
instrumentType: INSTRUMENT_TYPE[instrumentType] !== undefined ? INSTRUMENT_TYPE[instrumentType] : 0,
|
|
channel: channel === undefined ? 0 : channel
|
|
});
|
|
return true;
|
|
} catch (e) {
|
|
console.warn('[BridgeService] loadInstrument failed:', e);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 2. UNIFIED MIDI EVENT DISPATCH
|
|
* cmd: 'NOTE_ON' | 'NOTE_OFF' | 'CC' | 'PROGRAM' | 'PITCH_BEND'
|
|
* velocity 0..1; sampleOffset in samples within the current audio block.
|
|
* data2/data3 (A12): CC value / program / PB LSB|MSB.
|
|
*/
|
|
dispatchMidiEvent: function (cmd, channel, pitch, velocity, sampleOffset, data2, data3) {
|
|
if (!this._tauri()) return false;
|
|
var safeVelocity = Math.floor(Math.min(1.0, Math.max(0.0, velocity)) * 127);
|
|
var byteCmd;
|
|
switch (cmd) {
|
|
case 'CC': byteCmd = 0xB; break;
|
|
case 'PROGRAM': byteCmd = 0xC; break;
|
|
case 'PITCH_BEND': byteCmd = 0xE; break;
|
|
default: byteCmd = cmd === 'NOTE_ON' ? 0x9 : 0x8; break;
|
|
}
|
|
window.__TAURI__.core.invoke('push_midi_event', {
|
|
command: byteCmd,
|
|
channel: channel,
|
|
pitch: pitch,
|
|
velocity: safeVelocity,
|
|
data2: data2 === undefined ? 0 : data2,
|
|
data3: data3 === undefined ? 0 : data3,
|
|
sampleOffset: sampleOffset || 0
|
|
}).catch(function (e) { console.warn('[BridgeService] push_midi_event:', e); });
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* 3. OPEN FLOATING CHILD WINDOW NATIVE GUI
|
|
*/
|
|
openNativeGUI: async function (pluginId) {
|
|
if (!this._tauri()) return false;
|
|
try {
|
|
await window.__TAURI__.core.invoke('open_vst_gui', { pluginId: pluginId });
|
|
return true;
|
|
} catch (e) {
|
|
console.warn('[BridgeService] open_vst_gui:', e);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 4. TRANSPORT CONTROL ('play' | 'stop' | 'panic' | 'set_position')
|
|
* playhead: sample position (A13) — used by 'play' and 'set_position'.
|
|
*/
|
|
transport: function (kind, playhead) {
|
|
if (!this._tauri()) return false;
|
|
var args = { kind: kind };
|
|
if (playhead !== undefined) args.playhead = playhead;
|
|
window.__TAURI__.core.invoke('transport_control', args)
|
|
.catch(function (e) { console.warn('[BridgeService] transport_control:', e); });
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* 5. AUDIO SINK: register consumer of PCM frames from the bridge.
|
|
* cb(l: Float32Array, r: Float32Array)
|
|
*/
|
|
onAudio: function (cb) {
|
|
this._audioCb = cb;
|
|
}
|
|
};
|
|
|
|
window.NativeBridgeService = service;
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', function () { service._init(); });
|
|
} else {
|
|
service._init();
|
|
}
|
|
})();
|