Skip to main content

maolan_plugin_protocol/
protocol.rs

1use std::sync::atomic::{AtomicU32, Ordering};
2
3/// Magic number: "MAOL" in big-endian ASCII.
4pub const MAGIC: u32 = 0x4D41_4F4C;
5
6/// Current protocol version.
7pub const VERSION: u32 = 1;
8
9/// Maximum number of audio channels (main + sidechain combined).
10pub const MAX_CHANNELS: usize = 32;
11
12/// Number of audio buses (main + sidechain).
13pub const NUM_BUSES: usize = 2;
14
15/// Maximum audio block size in samples.
16pub const MAX_BLOCK_SIZE: usize = 4096;
17
18/// Capacity of each ring buffer in slots (power of two).
19pub const RING_CAPACITY: usize = 4096;
20
21// --- Section sizes ---
22pub const HEADER_SIZE: usize = 256;
23pub const CONTROL_SIZE: usize = 256;
24pub const AUDIO_BUFFER_SIZE: usize = MAX_CHANNELS * NUM_BUSES * MAX_BLOCK_SIZE * 4; // f32
25pub const PARAM_RING_SIZE: usize = RING_CAPACITY * std::mem::size_of::<ParameterEvent>();
26pub const MIDI_RING_SIZE: usize = RING_CAPACITY * std::mem::size_of::<MidiEvent>();
27pub const TRANSPORT_SIZE: usize = 256;
28pub const SCRATCH_SIZE: usize = 65536;
29
30// --- Offsets into the shared-memory segment ---
31/// Control area starts right after the header.
32pub const CONTROL_OFFSET: usize = HEADER_SIZE;
33/// Audio buffers start after the control area.
34pub const AUDIO_OFFSET: usize = HEADER_SIZE + CONTROL_SIZE;
35/// Parameter ring buffer.
36pub const PARAM_RING_OFFSET: usize = AUDIO_OFFSET + AUDIO_BUFFER_SIZE;
37/// MIDI ring buffer.
38pub const MIDI_RING_OFFSET: usize = PARAM_RING_OFFSET + PARAM_RING_SIZE;
39pub const ECHO_RING_OFFSET: usize = MIDI_RING_OFFSET + MIDI_RING_SIZE;
40pub const ECHO_RING_SIZE: usize = RING_CAPACITY * std::mem::size_of::<ParameterEvent>();
41/// Transport state block (256-byte aligned from here).
42pub const TRANSPORT_OFFSET: usize = {
43    let end = ECHO_RING_OFFSET + ECHO_RING_SIZE;
44    // Align up to 256 bytes
45    (end + 255) & !255
46};
47/// State blob scratch area.
48pub const SCRATCH_OFFSET: usize = TRANSPORT_OFFSET + TRANSPORT_SIZE;
49
50/// Total bytes actively used by the protocol layout.
51pub const LAYOUT_SIZE: usize = SCRATCH_OFFSET + SCRATCH_SIZE;
52
53/// Total shared-memory allocation size (4 MiB, page-aligned).
54pub const SHM_SIZE: usize = 4 * 1024 * 1024;
55
56// --- Control-area indices (all 4-byte atomics inside CONTROL_OFFSET..CONTROL_OFFSET+256) ---
57pub const PARAM_WRITE_IDX_OFFSET: usize = CONTROL_OFFSET;
58pub const PARAM_READ_IDX_OFFSET: usize = CONTROL_OFFSET + 4;
59pub const MIDI_WRITE_IDX_OFFSET: usize = CONTROL_OFFSET + 8;
60pub const MIDI_READ_IDX_OFFSET: usize = CONTROL_OFFSET + 12;
61pub const ECHO_WRITE_IDX_OFFSET: usize = CONTROL_OFFSET + 16;
62pub const ECHO_READ_IDX_OFFSET: usize = CONTROL_OFFSET + 20;
63
64// --- Structs ---
65
66pub const PARAM_EVENT_VALUE: u32 = 0;
67pub const PARAM_EVENT_MOD: u32 = 1;
68pub const PARAM_EVENT_GESTURE_BEGIN: u32 = 2;
69pub const PARAM_EVENT_GESTURE_END: u32 = 3;
70
71/// Fixed-size parameter change event (16 bytes, 16-byte aligned).
72#[repr(C, align(16))]
73#[derive(Clone, Copy, Debug, Default)]
74pub struct ParameterEvent {
75    pub param_index: u32,
76    pub value: f32,
77    pub sample_offset: u32,
78    pub event_kind: u32,
79}
80
81/// Fixed-size MIDI event (16 bytes, 16-byte aligned).
82#[repr(C, align(16))]
83#[derive(Clone, Copy, Debug, Default)]
84pub struct MidiEvent {
85    pub sample_offset: u32,
86    pub data: [u8; 3],
87    pub channel: u8,
88    pub flags: u16,
89    pub _pad: u16,
90}
91
92/// Transport state block (256 bytes).
93#[repr(C, align(256))]
94#[derive(Clone, Copy, Debug)]
95pub struct TransportState {
96    pub playhead_sample: u64,
97    pub tempo: f64,
98    pub numerator: u32,
99    pub denominator: u32,
100    pub flags: u32,
101    pub sample_rate_hz: f64,
102    _pad: [u8; 256 - 40],
103}
104
105impl Default for TransportState {
106    fn default() -> Self {
107        Self {
108            playhead_sample: 0,
109            tempo: 120.0,
110            numerator: 4,
111            denominator: 4,
112            flags: 0,
113            sample_rate_hz: 0.0,
114            _pad: [0; 256 - 40],
115        }
116    }
117}
118
119/// Shared-memory header (256 bytes).
120#[repr(C, align(256))]
121pub struct ShmHeader {
122    pub magic: u32,
123    pub version: u32,
124    pub flags: u32,
125    pub ready: AtomicU32,
126    pub heartbeat: AtomicU32,
127    pub error_code: u32,
128    pub shutdown_request: AtomicU32,
129    pub tasks_issued: AtomicU32,
130    pub tasks_completed: AtomicU32,
131    pub block_size: AtomicU32,
132    pub num_input_channels: AtomicU32,
133    pub num_output_channels: AtomicU32,
134    /// Request type: 0 = none, 1 = save_state, 2 = restore_state, 3 = gui_show, 4 = gui_hide
135    pub request_type: AtomicU32,
136    /// Request status: 0 = pending, 1 = success, 2 = error
137    pub request_status: AtomicU32,
138    /// Valid bytes in scratch area for state operations
139    pub scratch_size: AtomicU32,
140    /// Parent window ID for GUI embedding (X11 window ID on Unix)
141    pub parent_window: AtomicU32,
142    _pad: [u8; 256 - 64],
143}
144
145impl Default for ShmHeader {
146    fn default() -> Self {
147        Self {
148            magic: MAGIC,
149            version: VERSION,
150            flags: 0,
151            ready: AtomicU32::new(0),
152            heartbeat: AtomicU32::new(0),
153            error_code: 0,
154            shutdown_request: AtomicU32::new(0),
155            tasks_issued: AtomicU32::new(0),
156            tasks_completed: AtomicU32::new(0),
157            block_size: AtomicU32::new(0),
158            num_input_channels: AtomicU32::new(0),
159            num_output_channels: AtomicU32::new(0),
160            request_type: AtomicU32::new(0),
161            request_status: AtomicU32::new(0),
162            scratch_size: AtomicU32::new(0),
163            parent_window: AtomicU32::new(0),
164            _pad: [0; 256 - 64],
165        }
166    }
167}
168
169// --- Layout helpers ---
170
171/// Zero-initialize the entire shared-memory region and write the header.
172///
173/// # Safety
174/// `ptr` must be a valid pointer to a memory region of `size` bytes.
175pub unsafe fn init_shm_layout(ptr: *mut u8, size: usize) {
176    unsafe {
177        std::ptr::write_bytes(ptr, 0, size);
178        let header = ptr as *mut ShmHeader;
179        std::ptr::write(header, ShmHeader::default());
180    }
181}
182
183/// Returns a reference to the header at the start of the mapping.
184///
185/// # Safety
186/// `ptr` must point to a valid allocation containing at least `ShmHeader`'s size.
187pub unsafe fn header_ref(ptr: *mut u8) -> &'static ShmHeader {
188    unsafe { &*(ptr as *mut ShmHeader) }
189}
190
191/// Returns a mutable reference to the header.
192///
193/// # Safety
194/// `ptr` must point to a valid allocation containing at least `ShmHeader`'s size.
195pub unsafe fn header_mut(ptr: *mut u8) -> &'static mut ShmHeader {
196    unsafe { &mut *(ptr as *mut ShmHeader) }
197}
198
199/// Returns a pointer to the audio buffer region.
200///
201/// # Safety
202/// `ptr` must point to an allocation large enough to contain the audio buffer.
203pub unsafe fn audio_ptr(ptr: *mut u8) -> *mut f32 {
204    unsafe { ptr.add(AUDIO_OFFSET) as *mut f32 }
205}
206
207/// Returns a pointer to a specific channel/bus plane.
208///
209/// `channel` is 0-based up to `MAX_CHANNELS - 1`.
210/// `bus` is 0 (main) or 1 (sidechain).
211///
212/// # Safety
213/// `ptr` must point to a valid allocation large enough to contain the audio data.
214pub unsafe fn audio_channel_ptr(ptr: *mut u8, channel: usize, bus: usize) -> *mut f32 {
215    let plane_size = MAX_BLOCK_SIZE * std::mem::size_of::<f32>();
216    let offset = AUDIO_OFFSET + (channel * NUM_BUSES + bus) * plane_size;
217    unsafe { ptr.add(offset) as *mut f32 }
218}
219
220/// Returns a pointer to the parameter ring buffer slot array.
221///
222/// # Safety
223/// `ptr` must point to a valid allocation large enough to contain the parameter ring.
224pub unsafe fn param_ring_ptr(ptr: *mut u8) -> *mut ParameterEvent {
225    unsafe { ptr.add(PARAM_RING_OFFSET) as *mut ParameterEvent }
226}
227
228/// Returns pointers to the parameter ring write/read atomics.
229///
230/// # Safety
231/// `ptr` must point to a valid allocation containing the parameter ring atomics.
232pub unsafe fn param_indices(ptr: *mut u8) -> (*mut AtomicU32, *mut AtomicU32) {
233    unsafe {
234        (
235            ptr.add(PARAM_WRITE_IDX_OFFSET) as *mut AtomicU32,
236            ptr.add(PARAM_READ_IDX_OFFSET) as *mut AtomicU32,
237        )
238    }
239}
240
241/// Returns a pointer to the MIDI ring buffer slot array.
242///
243/// # Safety
244/// `ptr` must point to a valid allocation large enough to contain the MIDI ring.
245pub unsafe fn midi_ring_ptr(ptr: *mut u8) -> *mut MidiEvent {
246    unsafe { ptr.add(MIDI_RING_OFFSET) as *mut MidiEvent }
247}
248
249/// Returns pointers to the MIDI ring write/read atomics.
250///
251/// # Safety
252/// `ptr` must point to a valid allocation containing the MIDI ring atomics.
253pub unsafe fn midi_indices(ptr: *mut u8) -> (*mut AtomicU32, *mut AtomicU32) {
254    unsafe {
255        (
256            ptr.add(MIDI_WRITE_IDX_OFFSET) as *mut AtomicU32,
257            ptr.add(MIDI_READ_IDX_OFFSET) as *mut AtomicU32,
258        )
259    }
260}
261
262/// Returns a pointer to the echo ring buffer slot array.
263///
264/// # Safety
265/// `ptr` must point to a valid allocation large enough to contain the echo ring.
266pub unsafe fn echo_ring_ptr(ptr: *mut u8) -> *mut ParameterEvent {
267    unsafe { ptr.add(ECHO_RING_OFFSET) as *mut ParameterEvent }
268}
269
270/// Returns pointers to the echo ring write/read atomics.
271///
272/// # Safety
273/// `ptr` must point to a valid allocation containing the echo ring atomics.
274pub unsafe fn echo_indices(ptr: *mut u8) -> (*mut AtomicU32, *mut AtomicU32) {
275    unsafe {
276        (
277            ptr.add(ECHO_WRITE_IDX_OFFSET) as *mut AtomicU32,
278            ptr.add(ECHO_READ_IDX_OFFSET) as *mut AtomicU32,
279        )
280    }
281}
282
283/// Returns a reference to the transport state.
284///
285/// # Safety
286/// `ptr` must point to a valid allocation containing at least `TransportState`'s size.
287pub unsafe fn transport_ref(ptr: *mut u8) -> &'static TransportState {
288    unsafe { &*(ptr.add(TRANSPORT_OFFSET) as *mut TransportState) }
289}
290
291/// Returns a mutable reference to the transport state.
292///
293/// # Safety
294/// `ptr` must point to a valid allocation containing at least `TransportState`'s size.
295pub unsafe fn transport_mut(ptr: *mut u8) -> &'static mut TransportState {
296    unsafe { &mut *(ptr.add(TRANSPORT_OFFSET) as *mut TransportState) }
297}
298
299/// Returns a pointer to the scratch buffer region.
300///
301/// # Safety
302/// `ptr` must point to an allocation large enough to contain the scratch buffer.
303pub unsafe fn scratch_ptr(ptr: *mut u8) -> *mut u8 {
304    unsafe { ptr.add(SCRATCH_OFFSET) }
305}
306
307// --- Static assertions for sizes ---
308
309const _: () = assert!(std::mem::size_of::<ShmHeader>() == 256);
310const _: () = assert!(std::mem::align_of::<ShmHeader>() == 256);
311const _: () = assert!(std::mem::size_of::<ParameterEvent>() == 16);
312const _: () = assert!(std::mem::align_of::<ParameterEvent>() == 16);
313const _: () = assert!(std::mem::size_of::<MidiEvent>() == 16);
314const _: () = assert!(std::mem::align_of::<MidiEvent>() == 16);
315const _: () = assert!(std::mem::size_of::<TransportState>() == 256);
316const _: () = assert!(std::mem::align_of::<TransportState>() == 256);
317const _: () = assert!(LAYOUT_SIZE <= SHM_SIZE);
318
319/// Wait (spin + yield) until `ready` becomes non-zero or timeout elapses.
320pub fn wait_for_ready(header: &ShmHeader, timeout: std::time::Duration) -> bool {
321    let start = std::time::Instant::now();
322    while header.ready.load(Ordering::Acquire) == 0 {
323        if start.elapsed() >= timeout {
324            return false;
325        }
326        std::thread::yield_now();
327    }
328    true
329}