Files
SonicForgeStudio/app/static/js/services/nativeBridgeService.js
T
locpham c3368d0a91 feat: native host bridge integration — C++ bridge, Rust SHM, JS routing, build scripts, docs
- native_bridge/: InstrumentEngineManager multi-channel, sample-accurate, CC/program/pitchbend, transport, Vst3Instrument stub (HAVE_VST3SDK)
- src-tauri: shm.rs, bridge spawn + audio pump + health monitor, open_vst_gui, externalBin, commands
- app: UnifiedMidiRouter, NativeBridgeService, bridgeAudioNode, audioRoutingEngine, Plugin Manager UI, Bridge/WASM indicator, set_position sync
- build: 3 ps1 (force-added, build/ ignored), verify_bundle --check-bridge, CI workflow
- docs: TASKS.md, TEST_NOTES.md (Windows verify checklist), install/report updates
2026-08-11 23:37:29 +07:00

151 lines
5.4 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 });
},
_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) {
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();
}
})();