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
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
// 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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user