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
+11
View File
@@ -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
+243 -222
View File
@@ -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<Self> {
if !cfg!(windows) {
return None;
}
let name_wide: Vec<u16> = shm_name().encode_utf16().chain(std::iter::once(0)).collect();
let size = std::mem::size_of::<SharedAudioBufferIPC>() 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<Option<Shm>>);
#[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::<MidiEventIPC>(), 12);
assert_eq!(std::mem::size_of::<ControlEventIPC>(), 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::<SharedAudioBufferIPC>(), 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<Self> {
if !cfg!(windows) {
return None;
}
let name_wide: Vec<u16> = shm_name().encode_utf16().chain(std::iter::once(0)).collect();
let size = std::mem::size_of::<SharedAudioBufferIPC>() 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<MidiEventIPC> {
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<Option<Shm>>);
#[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::<MidiEventIPC>(), 12);
assert_eq!(std::mem::size_of::<ControlEventIPC>(), 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::<SharedAudioBufferIPC>(), 28328);
}
}