c3368d0a91
- 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
151 lines
5.4 KiB
JavaScript
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();
|
|
}
|
|
})();
|