A14: MIDI-out từ plugin (VST2/VST3) → SHM → Rust emit → DAW track

- Vst2Instrument: audioMaster callback thu VstEvents, push SHM midiOutQueue
- Vst3Instrument: IEventList output bus (kAudioMasterWantMidi) — check
- SharedMemoryIPC.h: midiOutQueue[64] + midiOutCount (offset 27556/28324, sizeof 28328)
- shm.rs: MidiEventIPC Serialize, drain_midi_out() emit 'bridge-midi-out'
- lib.rs: pump emit midi-out events
- nativeBridgeService.js: onMidiOut API
- app.jsx: route MIDI-out về selected track qua SonicMidiRouter (feedback guard)
- TASKS.md: A14 ✅
This commit is contained in:
2026-08-19 21:12:07 +07:00
parent 6efef85e47
commit 6a3b430da0
10 changed files with 375 additions and 226 deletions
+35
View File
@@ -1797,6 +1797,38 @@ function getAudioContext() {
}
window.BridgeAudioNode.init(audioCtx);
window.NativeBridgeService.onAudio(function (l, r) { window.BridgeAudioNode.onAudio(l, r); });
// A14: plugin MIDI output -> route notes to the selected track
// (Scaler2/Instacomposer chords play the selected track's
// instrument). Feedback guard: skip events whose source channel
// is the selected track's own channel (would loop generator into
// itself). command is the VST2 status high nibble (0x90 etc.).
window.NativeBridgeService.onMidiOut(function (events) {
if (!events || !events.length) return;
var ref = window.__selectedTrackIdRef;
var tId = (ref && ref.current) ? ref.current : '1';
for (var i = 0; i < events.length; i++) {
var ev = events[i];
var cmd;
switch (ev.command) {
case 0x90: cmd = ev.velocity > 0 ? 'NOTE_ON' : 'NOTE_OFF'; break;
case 0x80: cmd = 'NOTE_OFF'; break;
case 0xB0: cmd = 'CC'; break;
case 0xC0: cmd = 'PROGRAM'; break;
case 0xE0: cmd = 'PITCH_BEND'; break;
default: continue;
}
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
var tCh = window.SonicMidiRouter.allocateChannel(tId, false);
if (tCh === ev.channel) continue; // feedback guard
window.SonicMidiRouter.pushEvent({
cmd: cmd, channel: tId, pitch: ev.pitch,
velocity: ev.velocity / 127, data2: ev.data2, data3: ev.data3,
sampleOffset: ev.sample_offset || 0, live: true
});
}
console.log('[Bridge] MIDI-out', cmd, 'ch', ev.channel, 'note', ev.pitch, 'vel', ev.velocity, '-> track', tId);
}
});
if (window.AudioRoutingEngine) window.AudioRoutingEngine.connect(window.BridgeAudioNode, null, null);
}
} catch (e) {
@@ -18976,6 +19008,9 @@ const App = () => {
selectedClipIdRef.current = selectedClipId;
const selectedTrackIdRef = useRef(selectedTrackId);
selectedTrackIdRef.current = selectedTrackId;
// A14: plugin MIDI-out routing target — bootstrap handler (module scope)
// reads the live selection through this window ref (avoid stale closure).
window.__selectedTrackIdRef = selectedTrackIdRef;
const handleDeleteTrackRef = useRef(() => {});
const handleSplitTrackRef = useRef(() => {});
const handleRecordClickRef = useRef(() => {});
@@ -11,6 +11,7 @@
activeInstrumentType: 'VST3',
_audioCb: null,
_statusCb: null,
_midiOutCb: null,
_tauri: function () { return window.__TAURI__; },
/** D10: UI subscribes to bridge connection changes. cb({connected}) or null to clear. */
@@ -58,6 +59,12 @@
if (!self.isBridgeConnected) self._reconnectIfNeeded();
if (self._audioCb) self._audioCb(e.payload.l, e.payload.r);
});
// A14: plugin MIDI output (VstEvents -> SHM midiOutQueue -> pump).
// Payload: array of {command, channel, pitch, velocity, data2,
// data3, reserved, sample_offset}. JS routes notes to DAW tracks.
window.__TAURI__.event.listen('bridge-midi-out', function (e) {
if (self._midiOutCb) self._midiOutCb(e.payload);
});
window.__TAURI__.event.listen('bridge-down', function () {
self._notifyStatus(false);
if (window.SonicMidiRouter) window.SonicMidiRouter.setBridgeConnected(false);
@@ -187,6 +194,13 @@
*/
onAudio: function (cb) {
this._audioCb = cb;
},
/** A14: register consumer of plugin MIDI output.
* cb(events): array of {command, channel, pitch, velocity, data2,
* data3, reserved, sample_offset} — route to DAW tracks. */
onMidiOut: function (cb) {
this._midiOutCb = cb;
}
};