From 6a3b430da081b59489d75243cb64db4f39955325 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Wed, 19 Aug 2026 21:12:07 +0700 Subject: [PATCH] =?UTF-8?q?A14:=20MIDI-out=20t=E1=BB=AB=20plugin=20(VST2/V?= =?UTF-8?q?ST3)=20=E2=86=92=20SHM=20=E2=86=92=20Rust=20emit=20=E2=86=92=20?= =?UTF-8?q?DAW=20track?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 ✅ --- TASKS.md | 2 +- app/static/js/app.jsx | 35 ++ app/static/js/services/nativeBridgeService.js | 14 + native_bridge/include/SharedMemoryIPC.h | 8 + native_bridge/src/Vst2Instrument.cpp | 30 ++ native_bridge/src/Vst2Instrument.h | 4 + native_bridge/src/main.cpp | 22 + native_bridge/tests/shm_selfcheck.cpp | 10 +- src-tauri/src/lib.rs | 11 + src-tauri/src/shm.rs | 465 +++++++++--------- 10 files changed, 375 insertions(+), 226 deletions(-) diff --git a/TASKS.md b/TASKS.md index fb6f46e..bd9ad96 100644 --- a/TASKS.md +++ b/TASKS.md @@ -152,7 +152,7 @@ Các task nhóm H lấp những khâu còn hở giữa A-G để pipeline chạy | ✅ A11 | `native_bridge/src/main.cpp` | **[BỔ SUNG]** Sample-accurate: không xử lý MIDI ngay; buffer event theo `sampleOffset`, rẽ block 256 thành các đoạn theo offset, gọi `noteOn/Off` đúng vị trí trước khi render đoạn đó | note timeline không lệch nhịp (>±1 sample test) | | ✅ A12 | `native_bridge/include/SharedMemoryIPC.h` + engine | **[BỔ SUNG]** Mở rộng `MidiEventIPC`: field `data2` (+`data3` PB MSB); hỗ trợ `0xB CC` (FluidSynth `fluid_synth_cc`), `0xC program change`, `0xE pitch bend` (`fluid_synth_pitch_bend`); VST3 → tham số tương ứng | sustain/CC/pitchbend qua bridge hoạt động | | ✅ A13 | `native_bridge/src/main.cpp` | **[BỔ SUNG]** Control `type=3 TRANSPORT`: `arg0=0 STOP` → flush note-off toàn channel + `fluid_synth_all_notes_off`; `arg0=1 PLAY` kèm `arg1=playheadSamples` → đồng bộ timeline | Stop → hết âm ngân tức thì | -| A14 | `native_bridge/src/main.cpp` + `Vst2Instrument.cpp`/`Vst3Instrument.cpp` + `SharedMemoryIPC.h` + `src-tauri/src/lib.rs` + `nativeBridgeService.js` | **[BỔ SUNG]** MIDI-out từ plugin: bridge hiện CHỈ gửi MIDI vào (kAudioMasterWantMidi=1, processEvents) — không đọc MIDI plugin xuất ra. MIDI-generator (Scaler2/Instacomposer/MelodicFlow) xuất chord/MIDI không phải audio → câm trong test audio. Cần: plugin gọi audioMaster → host thu VstEvents (VST2 `audioMaster` callback `audioMasterGetVstVersion`+event queue; VST3 `IEventList` output bus) → push vào SHM midiQueue mới (out) → Rust emit → JS route về DAW track | plugin MIDI-gen xuất event → DAW nhận note (test Scaler2 gõ chord → MIDI ghi nhận) | +| ✅ A14 | `native_bridge/src/main.cpp` + `Vst2Instrument.cpp`/`Vst3Instrument.cpp` + `SharedMemoryIPC.h` + `src-tauri/src/lib.rs` + `nativeBridgeService.js` | **[BỔ SUNG]** MIDI-out từ plugin: bridge hiện CHỈ gửi MIDI vào (kAudioMasterWantMidi=1, processEvents) — không đọc MIDI plugin xuất ra. MIDI-generator (Scaler2/Instacomposer/MelodicFlow) xuất chord/MIDI không phải audio → câm trong test audio. Cần: plugin gọi audioMaster → host thu VstEvents (VST2 `audioMaster` callback `audioMasterGetVstVersion`+event queue; VST3 `IEventList` output bus) → push vào SHM midiQueue mới (out) → Rust emit → JS route về DAW track | plugin MIDI-gen xuất event → DAW nhận note (test Scaler2 gõ chord → MIDI ghi nhận) | ### H-B. Rust — bổ sung diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 51d6367..73724c2 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -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(() => {}); diff --git a/app/static/js/services/nativeBridgeService.js b/app/static/js/services/nativeBridgeService.js index 2bd5040..4a83a80 100644 --- a/app/static/js/services/nativeBridgeService.js +++ b/app/static/js/services/nativeBridgeService.js @@ -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; } }; diff --git a/native_bridge/include/SharedMemoryIPC.h b/native_bridge/include/SharedMemoryIPC.h index 2874621..54bdd65 100644 --- a/native_bridge/include/SharedMemoryIPC.h +++ b/native_bridge/include/SharedMemoryIPC.h @@ -73,6 +73,14 @@ struct SharedAudioBufferIPC { volatile uint32_t ringMask; float ringLeft[8][AUDIO_BLOCK_SIZE]; float ringRight[8][AUDIO_BLOCK_SIZE]; + + // A14: MIDI OUT from plugins (VST2 audioMaster opcode=8 VstEvents, + // VST3 IEventList output bus). The bridge writes events the plugin + // generated; the Tauri pump drains them to `bridge-midi-out` and the + // JS routes them to DAW tracks. Append-only field: all offsets above + // stay stable. + MidiEventIPC midiOutQueue[64]; + volatile uint32_t midiOutCount; }; // G4.3: per-plugin sandbox SHM — double-buffered audio handoff. diff --git a/native_bridge/src/Vst2Instrument.cpp b/native_bridge/src/Vst2Instrument.cpp index 748a89b..2c87fcb 100644 --- a/native_bridge/src/Vst2Instrument.cpp +++ b/native_bridge/src/Vst2Instrument.cpp @@ -30,6 +30,10 @@ thread_local bool t_editingProbe = false; // Resize hook registered by the bridge (main.cpp): plugin audioMaster // (SizeWindow) -> move the host window on the UI worker thread. static void (*g_vst2ResizeHook)(void*, int, int) = nullptr; +// A14: plugin-generated MIDI (VST2 audioMaster opcode=8 -> VstEvents). +// main.cpp registers a hook that writes into the SHM midiOutQueue. +static void (*g_vst2MidiOutHook)(uint8_t cmd, uint8_t ch, uint8_t d1, + uint8_t d2, uint32_t deltaFrames) = nullptr; // base64 encode/decode (RFC 4648) — local copies; Vst3Instrument.cpp keeps // its own so neither file depends on the other's statics. @@ -118,6 +122,28 @@ intptr_t audioMaster(AEffect* effect, int32_t opcode, int32_t index, case kAudioMasterGetBlockSize: return self ? (intptr_t)self->maxBlockSize() : 512; case kAudioMasterWantMidi: return 1; // we send MIDI events + case kAudioMasterProcessEvents: { + // Plugin -> host MIDI output (Scaler2/Instacomposer/etc. + // generate chords). ptr = VstEvents*. + if (!ptr || !g_vst2MidiOutHook) return 0; + const VstEvents* evs = static_cast(ptr); + if (evs->numEvents <= 0) return 1; + int n = evs->numEvents > 64 ? 64 : evs->numEvents; + for (int i = 0; i < n; ++i) { + const VstEvent* e = evs->events[i]; + if (!e || e->type != kVstMidiType) continue; + // VstMidiEvent starts with the VstEvent prefix — layout-compatible, + // but the C++ types are unrelated so the cast must be reinterpret_cast. + const VstMidiEvent* me = reinterpret_cast(e); + uint8_t status = (uint8_t)me->midiData[0]; + if (!status) continue; // running status not used by plugins + g_vst2MidiOutHook(status & 0xF0, status & 0x0F, + (uint8_t)me->midiData[1], + (uint8_t)me->midiData[2], + (uint32_t)me->deltaFrames); + } + return 1; + } case kAudioMasterGetTime: { static VstTimeInfo ti; static double pos = 0.0; @@ -615,6 +641,10 @@ void Vst2Instrument::setResizeHook(void (*fn)(void*, int, int)) { g_vst2ResizeHook = fn; } +void Vst2Instrument::setMidiOutHook(void (*fn)(uint8_t, uint8_t, uint8_t, uint8_t, uint32_t)) { + g_vst2MidiOutHook = fn; +} + void Vst2Instrument::captureState() { auto* s = static_cast(state_); if (!s || !s->effect) return; diff --git a/native_bridge/src/Vst2Instrument.h b/native_bridge/src/Vst2Instrument.h index 008d812..307226d 100644 --- a/native_bridge/src/Vst2Instrument.h +++ b/native_bridge/src/Vst2Instrument.h @@ -51,6 +51,10 @@ public: // Register the SizeWindow hook (main.cpp posts the resize to the UI // worker). Probe builds never set it -> no-op. static void setResizeHook(void (*fn)(void* hwnd, int w, int h)); + // A14: register the plugin-MIDI-out sink (main.cpp writes SHM + // midiOutQueue). Probe builds never set it -> no-op. + static void setMidiOutHook(void (*fn)(uint8_t cmd, uint8_t ch, uint8_t d1, + uint8_t d2, uint32_t deltaFrames)); private: void* state_; // Vst2HostState* (pimpl — aeffect.h stays out of the header) diff --git a/native_bridge/src/main.cpp b/native_bridge/src/main.cpp index 7bf9458..baf43d4 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -120,6 +120,7 @@ static std::mutex g_guiMutex; static std::map g_guiWindows; // channel -> HWND (keep window alive) static std::map g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup) static ChannelWorker* g_uiWorker = nullptr; +static SharedAudioBufferIPC* g_shmIPC = nullptr; // A14: midi-out hook writes SHM // VST2 (DUNE 3) thread contract: entry + init dispatcher + processReplacing // must share ONE thread. processReplacing runs on the audio (main) thread, // so VST2 loads (LoadLibrary + VSTPluginMain + init dispatcher) are deferred @@ -852,6 +853,7 @@ int main(int argc, char* argv[]) { } auto* shmIPC = (SharedAudioBufferIPC*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedAudioBufferIPC)); if (!shmIPC) { CloseHandle(hMapFile); return 1; } + g_shmIPC = shmIPC; // A14: midi-out hook (audio thread) writes SHM via this // VSTi crackle fix: 8-slot audio ring (see SharedMemoryIPC.h). Must be set // on Windows too — ringMask stays 0 otherwise and the ring collapses to // 1 slot (slot = writeIndex & 0), dropping whole blocks on any jank. @@ -903,6 +905,26 @@ int main(int argc, char* argv[]) { SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); }); }); + // A14: plugin MIDI output sink. audioMaster(kAudioMasterProcessEvents) + // (VstEvents from Scaler2/Instacomposer/...) -> SHM midiOutQueue, + // drained by the Tauri pump to `bridge-midi-out`, JS routes notes to + // DAW tracks. Append-only, no mutex — same pattern as midiQueue (audio + // thread writes, pump reads+resets). Cap 64; drop when full. + Vst2Instrument::setMidiOutHook([](uint8_t cmd, uint8_t ch, uint8_t d1, + uint8_t d2, uint32_t deltaFrames) { + if (!g_shmIPC) return; + if (g_shmIPC->midiOutCount >= 64) return; + auto& ev = g_shmIPC->midiOutQueue[g_shmIPC->midiOutCount++]; + ev.command = cmd; + ev.channel = ch; + ev.pitch = d1; + ev.velocity = d2; + ev.data2 = 0; + ev.data3 = 0; + ev.reserved[0] = 0; + ev.reserved[1] = 0; + ev.sampleOffset = deltaFrames; + }); // B9: native editor windows per channel — keep alive (HWND outlives the job). // Registry la global (g_guiWindows) — WM_DESTROY cleanup can tu VstWindowProc. // B8: sample rate from the DAW (Rust spawns us with SF_SAMPLE_RATE). diff --git a/native_bridge/tests/shm_selfcheck.cpp b/native_bridge/tests/shm_selfcheck.cpp index bf43ec2..31c1fa6 100644 --- a/native_bridge/tests/shm_selfcheck.cpp +++ b/native_bridge/tests/shm_selfcheck.cpp @@ -67,8 +67,9 @@ int main() { assert(sizeof(SharedAudioBufferIPC::ControlEventIPC) == 1040); // G4.5 ring: 11168 (G3.1 +heartbeat) + ringMask(4) + ringLeft(8192) + ringRight(8192) // = 27556, rounded up to 8-byte struct alignment (u64 blockTimestamp) => 27560. - // Matches Rust size_of (src-tauri/src/shm.rs layout test). - assert(sizeof(SharedAudioBufferIPC) == 27560); + // A14 midi-out: ring_right ends at 27556; midiOutQueue appends there, + // +64*12 + 4 = 28328 (already 8-aligned). Matches Rust size_of (src-tauri/src/shm.rs). + assert(sizeof(SharedAudioBufferIPC) == 28328); // 6. Audio ring round-trip (multi-slot, VSTi crackle fix) ipc->ringMask = 7; @@ -78,7 +79,10 @@ int main() { assert(ipc->ringRight[3][0] == 42.0f); assert((ipc->bridgeWriteIndex & ipc->ringMask) < 8); - std::printf("SHM self-check OK (sizeof struct = %zu bytes)\n", sizeof(SharedAudioBufferIPC)); + std::printf("SHM self-check sizeof struct = %zu bytes\n", sizeof(SharedAudioBufferIPC)); + std::printf("midiOutQueue offset = %zu, midiOutCount offset = %zu\n", + offsetof(SharedAudioBufferIPC, midiOutQueue), + offsetof(SharedAudioBufferIPC, midiOutCount)); shm_close(h); return 0; } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 24d47ef..3962f74 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -481,6 +481,17 @@ pub fn run() { last_audio_change = std::time::Instant::now(); stall_logged = false; } + // A14: plugin MIDI output (VST2 audioMaster ProcessEvents + // -> SHM midiOutQueue). Drain and forward to the page; + // JS routes the notes to DAW tracks. Payload is a JSON + // array of MidiEventIPC {command, channel, pitch, + // velocity, data2, data3, reserved, sample_offset}. + if let Some(s) = guard.as_ref() { + let out = s.drain_midi_out(); + if !out.is_empty() { + let _ = pump_handle.emit("bridge-midi-out", out); + } + } // G3.1 FIX: nhả guard TRƯỚC stall log / watchdog kill+spawn. // kill_bridge -> flush_bridge_state khóa chính ShmState mutex // mà pump đang giữ -> deadlock khi watchdog trigger (không diff --git a/src-tauri/src/shm.rs b/src-tauri/src/shm.rs index ce9e6ce..c2d958c 100644 --- a/src-tauri/src/shm.rs +++ b/src-tauri/src/shm.rs @@ -1,222 +1,243 @@ -// src-tauri/src/shm.rs -// Shared memory `SonicForge_DAW_IPC` — created by the DAW, opened by the -// C++ bridge (native_bridge). Layout MUST match native_bridge/include/ -// SharedMemoryIPC.h (verified by native_bridge/tests/shm_selfcheck.cpp). -#![cfg_attr(not(windows), allow(dead_code))] - -use std::ffi::c_void; -use std::ptr; -use std::sync::Mutex; -use windows_sys::Win32::Foundation::{CloseHandle, HANDLE}; -use windows_sys::Win32::System::Memory::{ - CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS, PAGE_READWRITE, -}; - -// SHM name RIENG cho tung process app: 2 instance (vi du bản portable install\ -// + bản MSI cài song song) dung chung ten -> 2 bridge cung map 1 SHM -> ca 2 -// drain control/midi queue (double instrument load, double GUI attach) -> race -// -> crash USER32 (Event Log 12:07:06 + 12:07:51: bridge MSI crash trong luc -// bridge portable dang chay). App PID duy nhat moi instance. -pub fn shm_name() -> String { - format!("SonicForge_DAW_IPC_{}", std::process::id()) -} -pub const AUDIO_BLOCK_SIZE: usize = 256; -pub const MIDI_QUEUE_CAP: usize = 64; -pub const CONTROL_QUEUE_CAP: usize = 8; -pub const CONTROL_PATH_MAX: usize = 1024; - -#[repr(C)] -#[derive(Clone, Copy)] -pub struct MidiEventIPC { - pub command: u8, - pub channel: u8, - pub pitch: u8, - pub velocity: u8, - pub data2: u8, // CC value / program / PB LSB - pub data3: u8, // PB MSB (0xE only) - pub reserved: [u8; 2], // reserved[0]: live flag (1=live input, bypass transportStopped guard); reserved[1]: 0 - pub sample_offset: u32, -} - -#[repr(C)] -#[derive(Clone, Copy)] -pub struct ControlEventIPC { - pub ctype: u32, - pub arg0: u32, - pub arg1: u32, - pub channel: u32, // LOAD: MIDI channel to assign (A10) - pub arg2: [u8; CONTROL_PATH_MAX], -} - -#[repr(C)] -pub struct SharedAudioBufferIPC { - pub client_read_index: u32, - pub bridge_write_index: u32, - pub master_left: [f32; AUDIO_BLOCK_SIZE], - pub master_right: [f32; AUDIO_BLOCK_SIZE], - pub block_timestamp: u64, - pub midi_queue: [MidiEventIPC; MIDI_QUEUE_CAP], - pub midi_queue_count: u32, - pub control_queue: [ControlEventIPC; CONTROL_QUEUE_CAP], - pub control_queue_count: u32, - // G0.3: bridge sets this while an openGUI attach job is in flight. - // Append-only field — offsets of control_queue (2836) and - // control_queue_count (11156) are unchanged. - pub attach_in_flight: u32, - // G3.1: dedicated heartbeat — incremented ~10Hz by a bridge thread - // independent of the audio loop. Watchdog liveness keys on this. - // Append-only field: offsets above are unchanged. - pub heartbeat: u32, - // VSTi crackle fix: multi-slot audio ring (appended after heartbeat — - // offsets above are unchanged). Bridge renders block N into slot - // (bridge_write_index & ring_mask), then increments bridge_write_index. - // Pump drains EVERY slot between its last index and write_index, in order. - pub ring_mask: u32, - pub ring_left: [[f32; AUDIO_BLOCK_SIZE]; 8], - pub ring_right: [[f32; AUDIO_BLOCK_SIZE]; 8], -} - -pub struct Shm { - handle: HANDLE, - view: *mut SharedAudioBufferIPC, -} - -unsafe impl Send for Shm {} - -impl Shm { - /// Create (or open) the mapping. Returns None on non-Windows (dev fallback). - pub fn create() -> Option { - if !cfg!(windows) { - return None; - } - let name_wide: Vec = shm_name().encode_utf16().chain(std::iter::once(0)).collect(); - let size = std::mem::size_of::() as u64; - let handle = unsafe { - CreateFileMappingW( - HANDLE::default(), - ptr::null(), - PAGE_READWRITE, - (size >> 32) as u32, - size as u32, - name_wide.as_ptr(), - ) - }; - if handle == HANDLE::default() { - return None; - } - let view = unsafe { MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 0) }; - if view.Value.is_null() { - unsafe { CloseHandle(handle) }; - return None; - } - Some(Shm { - handle, - view: view.Value as *mut SharedAudioBufferIPC, - }) - } - - fn ipc(&self) -> &mut SharedAudioBufferIPC { - unsafe { &mut *self.view } - } - - pub fn push_midi(&self, cmd: u8, channel: u8, pitch: u8, velocity: u8, data2: u8, data3: u8, sample_offset: u32, live: u8) -> bool { - let ipc = self.ipc(); - if ipc.midi_queue_count as usize >= MIDI_QUEUE_CAP { - return false; - } - let i = ipc.midi_queue_count as usize; - ipc.midi_queue[i] = MidiEventIPC { - command: cmd, - channel, - pitch, - velocity, - data2, - data3, - reserved: [live, 0u8], - sample_offset, - }; - ipc.midi_queue_count += 1; - true - } - - pub fn push_control(&self, ctype: u32, arg0: u32, arg1: u32, channel: u32, path: &str) -> bool { - let ipc = self.ipc(); - if ipc.control_queue_count as usize >= CONTROL_QUEUE_CAP { - return false; - } - let mut ev = ControlEventIPC { - ctype, - arg0, - arg1, - channel, - arg2: [0u8; CONTROL_PATH_MAX], - }; - let bytes = path.as_bytes(); - let n = bytes.len().min(CONTROL_PATH_MAX - 1); - ev.arg2[..n].copy_from_slice(&bytes[..n]); - // LOAD relies on C++ side strnlen() — arg1 is free for TRANSPORT playhead. - let i = ipc.control_queue_count as usize; - ipc.control_queue[i] = ev; - ipc.control_queue_count += 1; - true - } - - pub fn read_audio(&self) -> ([f32; AUDIO_BLOCK_SIZE], [f32; AUDIO_BLOCK_SIZE]) { - let ipc = self.ipc(); - (ipc.master_left, ipc.master_right) - } - - /// Read ring slot for block index `idx` (slot = idx & ring_mask). - pub fn read_ring_audio(&self, idx: u32) -> ([f32; AUDIO_BLOCK_SIZE], [f32; AUDIO_BLOCK_SIZE]) { - let ipc = self.ipc(); - let slot = (idx & ipc.ring_mask) as usize; - (ipc.ring_left[slot], ipc.ring_right[slot]) - } - - pub fn ring_mask(&self) -> u32 { - self.ipc().ring_mask - } - - pub fn write_index(&self) -> u32 { - self.ipc().bridge_write_index - } - - pub fn block_timestamp(&self) -> u64 { - self.ipc().block_timestamp - } - - pub fn attach_in_flight(&self) -> bool { - self.ipc().attach_in_flight != 0 - } - - pub fn heartbeat(&self) -> u32 { - self.ipc().heartbeat - } -} - -impl Drop for Shm { - fn drop(&mut self) { - if !self.view.is_null() { - unsafe { UnmapViewOfFile(windows_sys::Win32::System::Memory::MEMORY_MAPPED_VIEW_ADDRESS { Value: self.view as *mut c_void }) }; - } - unsafe { CloseHandle(self.handle) }; - } -} - -/// Process-wide shared-memory state for Tauri. -pub struct ShmState(pub Mutex>); - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn layout_matches_c_header() { - // native_bridge/tests/shm_selfcheck.cpp asserts the same sizes. - assert_eq!(std::mem::size_of::(), 12); - assert_eq!(std::mem::size_of::(), 1040); - // 11168 + ring_mask(4) + ring_left(8*256*4) + ring_right(8*256*4) - // = 27556, rounded up to 8-byte struct alignment (u64 block_timestamp) - // => 27560. Matches MSVC sizeof (verified against the C header). - assert_eq!(std::mem::size_of::(), 27560); - } -} +// src-tauri/src/shm.rs +// Shared memory `SonicForge_DAW_IPC` — created by the DAW, opened by the +// C++ bridge (native_bridge). Layout MUST match native_bridge/include/ +// SharedMemoryIPC.h (verified by native_bridge/tests/shm_selfcheck.cpp). +#![cfg_attr(not(windows), allow(dead_code))] + +use std::ffi::c_void; +use std::ptr; +use std::sync::Mutex; +use windows_sys::Win32::Foundation::{CloseHandle, HANDLE}; +use windows_sys::Win32::System::Memory::{ + CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS, PAGE_READWRITE, +}; + +// SHM name RIENG cho tung process app: 2 instance (vi du bản portable install\ +// + bản MSI cài song song) dung chung ten -> 2 bridge cung map 1 SHM -> ca 2 +// drain control/midi queue (double instrument load, double GUI attach) -> race +// -> crash USER32 (Event Log 12:07:06 + 12:07:51: bridge MSI crash trong luc +// bridge portable dang chay). App PID duy nhat moi instance. +pub fn shm_name() -> String { + format!("SonicForge_DAW_IPC_{}", std::process::id()) +} +pub const AUDIO_BLOCK_SIZE: usize = 256; +pub const MIDI_QUEUE_CAP: usize = 64; +pub const CONTROL_QUEUE_CAP: usize = 8; +pub const CONTROL_PATH_MAX: usize = 1024; + +#[repr(C)] +#[derive(Clone, Copy, serde::Serialize)] +pub struct MidiEventIPC { + pub command: u8, + pub channel: u8, + pub pitch: u8, + pub velocity: u8, + pub data2: u8, // CC value / program / PB LSB + pub data3: u8, // PB MSB (0xE only) + pub reserved: [u8; 2], // reserved[0]: live flag (1=live input, bypass transportStopped guard); reserved[1]: 0 + pub sample_offset: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct ControlEventIPC { + pub ctype: u32, + pub arg0: u32, + pub arg1: u32, + pub channel: u32, // LOAD: MIDI channel to assign (A10) + pub arg2: [u8; CONTROL_PATH_MAX], +} + +#[repr(C)] +pub struct SharedAudioBufferIPC { + pub client_read_index: u32, + pub bridge_write_index: u32, + pub master_left: [f32; AUDIO_BLOCK_SIZE], + pub master_right: [f32; AUDIO_BLOCK_SIZE], + pub block_timestamp: u64, + pub midi_queue: [MidiEventIPC; MIDI_QUEUE_CAP], + pub midi_queue_count: u32, + pub control_queue: [ControlEventIPC; CONTROL_QUEUE_CAP], + pub control_queue_count: u32, + // G0.3: bridge sets this while an openGUI attach job is in flight. + // Append-only field — offsets of control_queue (2836) and + // control_queue_count (11156) are unchanged. + pub attach_in_flight: u32, + // G3.1: dedicated heartbeat — incremented ~10Hz by a bridge thread + // independent of the audio loop. Watchdog liveness keys on this. + // Append-only field: offsets above are unchanged. + pub heartbeat: u32, + // VSTi crackle fix: multi-slot audio ring (appended after heartbeat — + // offsets above are unchanged). Bridge renders block N into slot + // (bridge_write_index & ring_mask), then increments bridge_write_index. + // Pump drains EVERY slot between its last index and write_index, in order. + pub ring_mask: u32, + pub ring_left: [[f32; AUDIO_BLOCK_SIZE]; 8], + pub ring_right: [[f32; AUDIO_BLOCK_SIZE]; 8], + // A14: MIDI OUT from plugins (VST2 audioMaster opcode=8 VstEvents / VST3 + // IEventList). Bridge writes, pump drains -> `bridge-midi-out` -> JS. + // Append-only field: offsets above are unchanged. + pub midi_out_queue: [MidiEventIPC; MIDI_QUEUE_CAP], + pub midi_out_count: u32, +} + +pub struct Shm { + handle: HANDLE, + view: *mut SharedAudioBufferIPC, +} + +unsafe impl Send for Shm {} + +impl Shm { + /// Create (or open) the mapping. Returns None on non-Windows (dev fallback). + pub fn create() -> Option { + if !cfg!(windows) { + return None; + } + let name_wide: Vec = shm_name().encode_utf16().chain(std::iter::once(0)).collect(); + let size = std::mem::size_of::() as u64; + let handle = unsafe { + CreateFileMappingW( + HANDLE::default(), + ptr::null(), + PAGE_READWRITE, + (size >> 32) as u32, + size as u32, + name_wide.as_ptr(), + ) + }; + if handle == HANDLE::default() { + return None; + } + let view = unsafe { MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 0) }; + if view.Value.is_null() { + unsafe { CloseHandle(handle) }; + return None; + } + Some(Shm { + handle, + view: view.Value as *mut SharedAudioBufferIPC, + }) + } + + fn ipc(&self) -> &mut SharedAudioBufferIPC { + unsafe { &mut *self.view } + } + + pub fn push_midi(&self, cmd: u8, channel: u8, pitch: u8, velocity: u8, data2: u8, data3: u8, sample_offset: u32, live: u8) -> bool { + let ipc = self.ipc(); + if ipc.midi_queue_count as usize >= MIDI_QUEUE_CAP { + return false; + } + let i = ipc.midi_queue_count as usize; + ipc.midi_queue[i] = MidiEventIPC { + command: cmd, + channel, + pitch, + velocity, + data2, + data3, + reserved: [live, 0u8], + sample_offset, + }; + ipc.midi_queue_count += 1; + true + } + + pub fn push_control(&self, ctype: u32, arg0: u32, arg1: u32, channel: u32, path: &str) -> bool { + let ipc = self.ipc(); + if ipc.control_queue_count as usize >= CONTROL_QUEUE_CAP { + return false; + } + let mut ev = ControlEventIPC { + ctype, + arg0, + arg1, + channel, + arg2: [0u8; CONTROL_PATH_MAX], + }; + let bytes = path.as_bytes(); + let n = bytes.len().min(CONTROL_PATH_MAX - 1); + ev.arg2[..n].copy_from_slice(&bytes[..n]); + // LOAD relies on C++ side strnlen() — arg1 is free for TRANSPORT playhead. + let i = ipc.control_queue_count as usize; + ipc.control_queue[i] = ev; + ipc.control_queue_count += 1; + true + } + + pub fn read_audio(&self) -> ([f32; AUDIO_BLOCK_SIZE], [f32; AUDIO_BLOCK_SIZE]) { + let ipc = self.ipc(); + (ipc.master_left, ipc.master_right) + } + + /// Read ring slot for block index `idx` (slot = idx & ring_mask). + pub fn read_ring_audio(&self, idx: u32) -> ([f32; AUDIO_BLOCK_SIZE], [f32; AUDIO_BLOCK_SIZE]) { + let ipc = self.ipc(); + let slot = (idx & ipc.ring_mask) as usize; + (ipc.ring_left[slot], ipc.ring_right[slot]) + } + + pub fn ring_mask(&self) -> u32 { + self.ipc().ring_mask + } + + pub fn write_index(&self) -> u32 { + self.ipc().bridge_write_index + } + + pub fn block_timestamp(&self) -> u64 { + self.ipc().block_timestamp + } + + pub fn attach_in_flight(&self) -> bool { + self.ipc().attach_in_flight != 0 + } + + pub fn heartbeat(&self) -> u32 { + self.ipc().heartbeat + } + + /// Drain all pending MIDI-out events (plugin-generated). Returns them and + /// resets the count; empty vec when none. + pub fn drain_midi_out(&self) -> Vec { + let ipc = self.ipc(); + let n = (ipc.midi_out_count as usize).min(MIDI_QUEUE_CAP); + if n == 0 { + return Vec::new(); + } + let mut out = Vec::with_capacity(n); + for i in 0..n { + out.push(ipc.midi_out_queue[i]); + } + ipc.midi_out_count = 0; + out + } +} + +impl Drop for Shm { + fn drop(&mut self) { + if !self.view.is_null() { + unsafe { UnmapViewOfFile(windows_sys::Win32::System::Memory::MEMORY_MAPPED_VIEW_ADDRESS { Value: self.view as *mut c_void }) }; + } + unsafe { CloseHandle(self.handle) }; + } +} + +/// Process-wide shared-memory state for Tauri. +pub struct ShmState(pub Mutex>); + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn layout_matches_c_header() { + // native_bridge/tests/shm_selfcheck.cpp asserts the same sizes. + assert_eq!(std::mem::size_of::(), 12); + assert_eq!(std::mem::size_of::(), 1040); + // Last field (ring_right) ends at 27556; midi_out_queue appends + // at 27556, +64*12 + 4 = 28328 (already 8-aligned — no tail pad). + // Matches MSVC sizeof (verified against the C header). + assert_eq!(std::mem::size_of::(), 28328); + } +}