ove/audio.rs
1// Copyright (C) 2026 Kamil Lulko <kamil.lulko@gmail.com>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4//
5// This file is part of oveRTOS.
6
7//! Audio graph engine for oveRTOS.
8//!
9//! Provides safe wrappers around the C graph API: build a DAG of audio nodes
10//! (sources, processors, sinks), validate formats, and execute in topological
11//! order.
12
13use crate::bindings;
14use crate::error::{Error, Result};
15use core::ffi::c_void;
16
17// SAFETY (module-wide contract for the `unsafe { bindings::ove_*(...) }` FFI
18// calls below): any handle passed to the C API is non-null and refers to a
19// live RTOS object — wrapper constructors establish validity via
20// `Error::from_code`, and `Drop` (or an explicit `deinit`) is the only place
21// a handle is released. Pointer and slice arguments reference caller-owned
22// memory valid for the duration of the call; the C side copies whatever it
23// retains and does not alias them past return (verified against the
24// signatures in `include/ove/*.h`). Blocks that deviate — `transmute`, raw
25// pointer casts from user data, slice reconstruction via `from_raw_parts`,
26// or storing a callback across the FFI boundary — carry their own
27// `// SAFETY:` comment.
28
29// ---------------------------------------------------------------------------
30// Pin tracker (debug-only) — mirrors `bindings/zig/ove/src/pin.zig::Tracker`.
31//
32// `Graph::build()` causes the C-side `ove_audio_graph` to record self-pointers
33// from `g->buffers[i].fmt` to `&g->nodes[i].out_fmt` (see
34// `backends/common/ove_audio_graph.c:273`). After that, moving the Graph in
35// Rust (e.g. `let g2 = g;`, returning by value out of an inner scope,
36// `Box::new(g)` after build, storing in a relocating container, etc.) leaves
37// those interior pointers dangling and silently corrupts subsequent audio-
38// thread reads.
39//
40// Rust can't easily enforce no-move at compile time without converting every
41// method to `Pin<&mut Self>` and breaking the documented
42// `let mut g = Graph::new()?;` ergonomics. Zig settled on a runtime address
43// tracker that panics in `Debug` builds and compiles away in release — we
44// mirror that here so the two bindings have matching safety stories.
45//
46// Lifecycle:
47// 1. `Graph::new()` constructs the tracker with no recorded address — moves
48// before `build()` are still safe and silent (no self-pointers yet).
49// 2. `Graph::build()` calls `tracker.record(self as *const _)`, capturing
50// the address at the moment self-pointers are established.
51// 3. `start` / `stop` / `process` call `tracker.assert_same(self as *const _)`
52// and panic on mismatch with a message naming the offender.
53// 4. `Drop` skips the assertion — `ove_audio_graph_deinit` only walks
54// `nodes[].ctx` and frees `buf_storage`, never the self-pointers, so
55// deinit at a moved address is benign.
56// ---------------------------------------------------------------------------
57
58/// Debug-build address tracker. Zero-sized in release; emits a `dmb`-cheap
59/// pointer compare in debug.
60#[cfg(debug_assertions)]
61#[derive(Default)]
62struct Tracker {
63 /// `Some(addr)` after `record()`; `None` between `new()` and `build()`.
64 /// Stored as a thin pointer (we only ever compare for equality —
65 /// never dereference — so `*const ()` is fine).
66 addr: Option<*const ()>,
67}
68
69#[cfg(debug_assertions)]
70impl Tracker {
71 /// Capture the current address of the wrapper. Called once from
72 /// `Graph::build()`; subsequent calls overwrite the recorded address
73 /// (harmless — a successful re-build by definition runs at a fresh
74 /// stable address).
75 #[inline]
76 fn record(&mut self, p: *const ()) {
77 self.addr = Some(p);
78 }
79
80 /// Assert that `p` matches the address recorded by `record()`.
81 /// No-op when nothing has been recorded yet (pre-build calls, e.g. a
82 /// caller that constructs a Graph but never builds it before drop).
83 ///
84 /// `type_name` is just a label baked into the panic message so the
85 /// failure says "audio::Graph" rather than "Graph (in some module)".
86 #[inline]
87 #[track_caller]
88 fn assert_same(&self, p: *const (), type_name: &str) {
89 if let Some(orig) = self.addr {
90 assert!(
91 orig == p,
92 "{type_name}: wrapper moved after build() — \
93 inited at {orig:p}, used at {p:p}. The C graph stores \
94 interior pointers (buffers[i].fmt → &nodes[i].out_fmt) \
95 during build(); moving the wrapper afterwards invalidates \
96 them and silently corrupts audio-thread reads. Pin the \
97 Graph in an InitCell / Box::leak / &'static mut before \
98 calling build()."
99 );
100 }
101 }
102}
103
104/// Release-build stub. Zero-sized — `core::mem::size_of::<Tracker>() == 0`
105/// — and every method inlines to nothing. `sizeof(Graph)` is identical
106/// between debug and release after `#[repr]` placement.
107#[cfg(not(debug_assertions))]
108#[derive(Default)]
109struct Tracker;
110
111#[cfg(not(debug_assertions))]
112impl Tracker {
113 #[inline(always)]
114 fn record(&mut self, _: *const ()) {}
115 #[inline(always)]
116 fn assert_same(&self, _: *const (), _: &str) {}
117}
118
119// SAFETY: the only field is an `Option<*const ()>` used purely as an
120// identity token — never dereferenced, never read for its target. The
121// surrounding `Graph` already declares `unsafe impl Send/Sync` (audio
122// thread takes the same address the control thread holds), and the
123// tracker's address-equality semantics are correct under Send/Sync.
124#[cfg(debug_assertions)]
125unsafe impl Send for Tracker {}
126#[cfg(debug_assertions)]
127unsafe impl Sync for Tracker {}
128
129/// Audio sample format.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum SampleFmt {
132 S16,
133 S32,
134 F32,
135}
136
137impl SampleFmt {
138 /// Convert to the raw C enum value (`ove_audio_sample_fmt`).
139 pub fn to_raw(self) -> u32 {
140 match self {
141 SampleFmt::S16 => 0,
142 SampleFmt::S32 => 1,
143 SampleFmt::F32 => 2,
144 }
145 }
146}
147
148/// Audio format descriptor.
149pub struct AudioFmt {
150 pub sample_rate: u32,
151 pub channels: u32,
152 pub sample_fmt: SampleFmt,
153}
154
155/// Initialize the audio graph.
156///
157/// # Errors
158/// Returns an error if `frames_per_period` is zero.
159pub fn graph_init(graph: &mut bindings::ove_audio_graph, frames_per_period: u32) -> Result<()> {
160 let rc = unsafe { bindings::ove_audio_graph_init(graph, frames_per_period) };
161 Error::from_code(rc)
162}
163
164/// Tear down the graph and release all resources.
165pub fn graph_deinit(graph: &mut bindings::ove_audio_graph) {
166 unsafe { bindings::ove_audio_graph_deinit(graph) };
167}
168
169/// Add a node to the graph. Returns the node index.
170///
171/// # Errors
172/// Returns an error if the graph is full or not in IDLE state.
173pub fn graph_add_node(
174 graph: &mut bindings::ove_audio_graph,
175 ops: &bindings::ove_audio_node_ops,
176 ctx: *mut c_void,
177 name: &core::ffi::CStr,
178 node_type: bindings::ove_audio_node_type,
179) -> core::result::Result<i32, Error> {
180 let rc =
181 unsafe { bindings::ove_audio_graph_add_node(graph, ops, ctx, name.as_ptr(), node_type) };
182 if rc < 0 {
183 Err(Error::from_code(rc).unwrap_err())
184 } else {
185 Ok(rc)
186 }
187}
188
189/// Connect two nodes. `from` feeds into `to`.
190///
191/// # Errors
192/// Returns an error on invalid indices or type violations.
193pub fn graph_connect(graph: &mut bindings::ove_audio_graph, from: u32, to: u32) -> Result<()> {
194 let rc = unsafe { bindings::ove_audio_graph_connect(graph, from, to) };
195 Error::from_code(rc)
196}
197
198/// Validate formats, resolve execution order, allocate buffers.
199///
200/// # Errors
201/// Returns an error on format mismatch, cycles, or OOM.
202pub fn graph_build(graph: &mut bindings::ove_audio_graph) -> Result<()> {
203 let rc = unsafe { bindings::ove_audio_graph_build(graph) };
204 Error::from_code(rc)
205}
206
207/// Start the graph (sink-driven mode).
208///
209/// # Errors
210/// Returns an error if graph is not in READY state.
211pub fn graph_start(graph: &mut bindings::ove_audio_graph) -> Result<()> {
212 let rc = unsafe { bindings::ove_audio_graph_start(graph) };
213 Error::from_code(rc)
214}
215
216/// Stop the graph.
217///
218/// # Errors
219/// Returns an error if graph is not in RUNNING state.
220pub fn graph_stop(graph: &mut bindings::ove_audio_graph) -> Result<()> {
221 let rc = unsafe { bindings::ove_audio_graph_stop(graph) };
222 Error::from_code(rc)
223}
224
225/// Process one cycle (app-driven mode).
226///
227/// # Errors
228/// Returns an error if graph is not built.
229pub fn graph_process(graph: &mut bindings::ove_audio_graph) -> Result<()> {
230 let rc = unsafe { bindings::ove_audio_graph_process(graph) };
231 Error::from_code(rc)
232}
233
234// ---------------------------------------------------------------------------
235// Audio device configuration builder
236// ---------------------------------------------------------------------------
237
238/// Build an `ove_audio_device_cfg` for I2S transport.
239///
240/// Constructs the C struct safely, including the transport-specific union
241/// fields, without requiring `unsafe` in application code.
242pub fn device_cfg_i2s(
243 sample_rate: u32,
244 channels: u32,
245 input_device: u32,
246) -> bindings::ove_audio_device_cfg {
247 let mut cfg: bindings::ove_audio_device_cfg = unsafe { core::mem::zeroed() };
248 cfg.transport = bindings::OVE_AUDIO_TRANSPORT_I2S;
249 cfg.fmt.sample_rate = sample_rate;
250 cfg.fmt.channels = channels;
251 cfg.fmt.sample_fmt = bindings::OVE_AUDIO_FMT_S16;
252 // Write i2s.input_device into the union via raw pointer
253 unsafe {
254 let union_ptr = &mut cfg as *mut _ as *mut u8;
255 let i2s_offset = core::mem::offset_of!(bindings::ove_audio_device_cfg, __bindgen_anon_1);
256 let i2s_ptr = union_ptr.add(i2s_offset) as *mut u32;
257 *i2s_ptr = input_device;
258 }
259 cfg
260}
261
262// ---------------------------------------------------------------------------
263// Safe Graph wrapper
264// ---------------------------------------------------------------------------
265
266/// Owned audio graph session.
267///
268/// Wraps the C `ove_audio_graph` in a safe API. All `unsafe` FFI calls
269/// are encapsulated — application code uses only safe methods.
270///
271/// # Example
272///
273/// ```ignore
274/// let mut g = Graph::new(512)?;
275/// let dev = device_cfg_i2s(16000, 1, 1);
276/// let src = g.device_source(&dev, b"mic\0")?;
277/// let proc = g.add_processor(PROC.get_mut(), b"dsp\0")?;
278/// let sink = g.device_sink(&dev, b"spk\0")?;
279/// g.connect(src, proc)?;
280/// g.connect(proc, sink)?;
281/// g.build()?;
282/// g.start()?;
283/// ```
284pub struct Graph {
285 inner: bindings::ove_audio_graph,
286 /// Debug-only address tracker; zero-sized in release. Records the
287 /// wrapper's address at `build()` time and panics if a later method
288 /// is called from a different address. See the `Tracker` block
289 /// above for the full rationale and lifecycle.
290 tracker: Tracker,
291}
292
293impl Graph {
294 /// Create and initialize a new audio graph.
295 ///
296 /// **Move policy:** moves are safe until [`Graph::build`] is called.
297 /// After build, the C graph stores self-pointers and the wrapper
298 /// must stay at a stable address — see [`Graph::build`] for details.
299 pub fn new(frames_per_period: u32) -> Result<Self> {
300 let mut inner: bindings::ove_audio_graph = unsafe { core::mem::zeroed() };
301 let rc = unsafe { bindings::ove_audio_graph_init(&mut inner, frames_per_period) };
302 Error::from_code(rc)?;
303 Ok(Self {
304 inner,
305 tracker: Tracker::default(),
306 })
307 }
308
309 /// Create and initialize a graph, attaching caller-owned buffer storage.
310 /// Required for `CONFIG_OVE_ZERO_HEAP` builds where `build()` cannot
311 /// `calloc` inter-node buffers. In heap-mode builds the storage is
312 /// unused but harmless. Prefer the [`crate::audio_graph!`] macro,
313 /// which emits the backing array automatically.
314 ///
315 /// Same move policy as [`Graph::new`].
316 pub fn new_with_storage(frames_per_period: u32, storage: &'static mut [u8]) -> Result<Self> {
317 let mut inner: bindings::ove_audio_graph = unsafe { core::mem::zeroed() };
318 let rc = unsafe { bindings::ove_audio_graph_init(&mut inner, frames_per_period) };
319 Error::from_code(rc)?;
320 let rc = unsafe {
321 bindings::ove_audio_graph_set_buf_storage(
322 &mut inner,
323 storage.as_mut_ptr() as *mut _,
324 storage.len(),
325 )
326 };
327 Error::from_code(rc)?;
328 Ok(Self {
329 inner,
330 tracker: Tracker::default(),
331 })
332 }
333
334 /// Add a hardware audio source node. Returns the node index.
335 pub fn device_source(
336 &mut self,
337 cfg: &bindings::ove_audio_device_cfg,
338 name: &[u8],
339 ) -> core::result::Result<u32, Error> {
340 let rc = unsafe {
341 bindings::ove_audio_device_source(&mut self.inner, cfg, name.as_ptr() as *const _)
342 };
343 if rc < 0 {
344 Err(Error::from_code(rc).unwrap_err())
345 } else {
346 Ok(rc as u32)
347 }
348 }
349
350 /// Add a hardware audio sink node. Returns the node index.
351 pub fn device_sink(
352 &mut self,
353 cfg: &bindings::ove_audio_device_cfg,
354 name: &[u8],
355 ) -> core::result::Result<u32, Error> {
356 let rc = unsafe {
357 bindings::ove_audio_device_sink(&mut self.inner, cfg, name.as_ptr() as *const _)
358 };
359 if rc < 0 {
360 Err(Error::from_code(rc).unwrap_err())
361 } else {
362 Ok(rc as u32)
363 }
364 }
365
366 /// Register a custom processor node. Returns the node index.
367 ///
368 /// Thin sugar over [`graph_add_processor`] — see that function's
369 /// rustdoc for the full lifetime / aliasing / drop-semantics
370 /// contract. The `&'static mut T` bound is load-bearing and must
371 /// not be satisfied by lifetime-laundering through `unsafe`.
372 pub fn add_processor<T: AudioProcessor>(
373 &mut self,
374 processor: &'static mut T,
375 name: &[u8],
376 ) -> core::result::Result<u32, Error> {
377 graph_add_processor(&mut self.inner, processor, name).map(|i| i as u32)
378 }
379
380 /// Connect two nodes. `from` feeds into `to`.
381 pub fn connect(&mut self, from: u32, to: u32) -> Result<()> {
382 graph_connect(&mut self.inner, from, to)
383 }
384
385 /// Validate formats, resolve execution order, allocate buffers.
386 ///
387 /// **Pinning:** this call records the wrapper's current address in
388 /// the debug-only tracker. After build, the C side stores
389 /// self-pointers (`buffers[i].fmt -> &nodes[i].out_fmt`) which
390 /// reference *this* address. Moving the Graph after build will be
391 /// caught by [`Graph::start`] / [`Graph::stop`] / [`Graph::process`]
392 /// with a panic in debug builds; in release the move silently
393 /// dangles those pointers, so callers must keep the wrapper in a
394 /// stable location (e.g. an `InitCell`, `Box::leak`, or
395 /// `&'static mut`).
396 pub fn build(&mut self) -> Result<()> {
397 // Record FIRST so a successful build but an out-of-place caller
398 // still gets caught on the next start/stop/process. The C call
399 // does not depend on the tracker being populated.
400 self.tracker.record(self as *const Self as *const ());
401 graph_build(&mut self.inner)
402 }
403
404 /// Start the graph (sink-driven mode).
405 pub fn start(&mut self) -> Result<()> {
406 self.tracker
407 .assert_same(self as *const Self as *const (), "audio::Graph");
408 graph_start(&mut self.inner)
409 }
410
411 /// Stop the graph.
412 pub fn stop(&mut self) -> Result<()> {
413 self.tracker
414 .assert_same(self as *const Self as *const (), "audio::Graph");
415 graph_stop(&mut self.inner)
416 }
417
418 /// Process one cycle (app-driven mode).
419 pub fn process(&mut self) -> Result<()> {
420 self.tracker
421 .assert_same(self as *const Self as *const (), "audio::Graph");
422 graph_process(&mut self.inner)
423 }
424}
425
426impl Drop for Graph {
427 fn drop(&mut self) {
428 graph_deinit(&mut self.inner);
429 }
430}
431
432// SAFETY: the C-side graph object is safe to hand off between threads once
433// built — audio-thread callbacks only read immutable node state, and control
434// APIs (`connect`, `start`, `stop`) are serialised by the caller. Required
435// so `Graph` can live inside `InitCell` / `InitMut` for FreeRTOS main-stack
436// survival (see the hiroic apps' `GRAPH` static).
437unsafe impl Send for Graph {}
438unsafe impl Sync for Graph {}
439
440// ---------------------------------------------------------------------------
441// Safe audio processor trait
442// ---------------------------------------------------------------------------
443
444/// Audio buffer descriptor (borrowed from the C layer).
445pub struct AudioBuf {
446 raw: *const bindings::ove_audio_buf,
447}
448
449impl AudioBuf {
450 /// Get a slice of interleaved S16 samples.
451 pub fn data_s16(&self) -> &[i16] {
452 // SAFETY: `self.raw` points to a live `ove_audio_buf` supplied by the
453 // graph for the duration of the processing callback; `data` is a
454 // `frames * channels`-element S16 buffer the C side keeps valid while
455 // the callback runs.
456 unsafe {
457 let buf = &*self.raw;
458 let count = buf.frames as usize * (*buf.fmt).channels as usize;
459 core::slice::from_raw_parts(buf.data as *const i16, count)
460 }
461 }
462
463 /// Get a mutable slice of interleaved S16 samples.
464 ///
465 /// `&self` is intentional: the underlying buffer is C-owned and the
466 /// AudioProcessor trait passes the output buf as `&AudioBuf` (so a
467 /// processor can read in / write out through one reference each).
468 /// Aliasing safety is the C side's responsibility.
469 #[allow(clippy::mut_from_ref)]
470 pub fn data_s16_mut(&self) -> &mut [i16] {
471 // SAFETY: as `data_s16`, but mutable — the graph hands the output
472 // buffer to the processor as `&AudioBuf` and the C side owns aliasing
473 // (read-in / write-out through separate references each cycle).
474 unsafe {
475 let buf = &*self.raw;
476 let count = buf.frames as usize * (*buf.fmt).channels as usize;
477 core::slice::from_raw_parts_mut(buf.data as *mut i16, count)
478 }
479 }
480
481 /// Number of frames in this buffer.
482 pub fn frames(&self) -> u32 {
483 unsafe { (*self.raw).frames }
484 }
485
486 /// Number of interleaved channels in this buffer.
487 pub fn channels(&self) -> u32 {
488 unsafe { (*(*self.raw).fmt).channels }
489 }
490}
491
492/// Trait for implementing custom audio processing nodes.
493///
494/// All FFI bridging is handled by the binding layer — implement this
495/// trait on a plain Rust struct with no `unsafe` or `extern "C"`.
496///
497/// # Example
498/// ```ignore
499/// struct MyProcessor { ... }
500/// impl ove::audio::AudioProcessor for MyProcessor {
501/// fn process(&mut self, input: &AudioBuf, output: &AudioBuf) {
502/// let src = input.data_s16();
503/// let dst = output.data_s16_mut();
504/// dst.copy_from_slice(src); // passthrough
505/// }
506/// }
507/// ```
508pub trait AudioProcessor {
509 /// Process one audio period. Called from the audio thread.
510 fn process(&mut self, input: &AudioBuf, output: &AudioBuf);
511}
512
513/// Register a custom processor node on the graph.
514///
515/// All FFI trampolines are generated internally — no `unsafe` or
516/// `extern "C"` needed in application code. The processor's `process`
517/// method is called from the audio thread for every period.
518///
519/// # Lifetime contract — `&'static mut T`
520///
521/// The bound on `processor` is `&'static mut T`, *not* `&mut T` with an
522/// inferred shorter lifetime. This is load-bearing:
523///
524/// - The audio thread holds a raw pointer to `T` (the registered
525/// context) and dereferences it as `&mut T` on every period. That
526/// pointer must remain valid until the [`Graph`] is dropped.
527/// - [`Graph::drop`] does **not** call into `T::drop` or release the
528/// processor's storage. The destroy slot in the C ops vtable is
529/// left `None` for processor nodes; `ove_audio_graph_deinit` simply
530/// walks past it. Lifetime management of `T` is entirely the
531/// caller's responsibility.
532///
533/// ## Recommended patterns
534///
535/// ```ignore
536/// // Preferred — works in both heap and zero-heap modes:
537/// static MY_PROC: InitMut<MyProc> = InitMut::new();
538/// MY_PROC.init(MyProc::new());
539/// // SAFETY: single owner — `MY_PROC` is only ever handed to the audio
540/// // graph; no other code touches it.
541/// let p: &'static mut MyProc = unsafe { MY_PROC.get_mut() };
542/// graph.add_processor(p, b"my\0")?;
543///
544/// // Heap-only:
545/// let p: &'static mut MyProc = Box::leak(Box::new(MyProc::new()));
546/// graph.add_processor(p, b"my\0")?;
547///
548/// // Older style, no allocator dependency:
549/// static mut PROC: MyProc = MyProc::new();
550/// graph.add_processor(unsafe { &mut PROC }, b"my\0")?;
551/// ```
552///
553/// ## Anti-patterns (compile but break the contract)
554///
555/// - **Lifetime laundering.** Do *not* `transmute` a shorter lifetime
556/// into `'static` to satisfy the bound:
557///
558/// ```ignore
559/// fn install(g: &mut Graph, p: &mut MyProc) -> Result<u32, Error> {
560/// // WRONG — `p` likely dies before the audio thread next runs:
561/// g.add_processor(unsafe { core::mem::transmute(p) }, b"p\0")
562/// }
563/// let mut local = MyProc::new();
564/// install(&mut g, &mut local)?; // `local` drops at scope end
565/// g.build()?; g.start()?; // audio thread → dangling pointer
566/// ```
567///
568/// - **Re-initialising an `InitMut` while registered.** The cell's
569/// storage outlives the program, but `InitMut::init()` constructs
570/// a fresh `T` on top of the old bytes. The audio thread's
571/// `&mut T` still points at the same address, now aliasing a
572/// freshly-initialised value — instant UB under Rust's `&mut`
573/// exclusivity rule. Unregister (drop the [`Graph`]) before
574/// re-initialising the cell.
575///
576/// ## Aliasing rule
577///
578/// While the processor is registered, the audio thread effectively
579/// holds an exclusive `&mut T` borrow for every period. The caller
580/// MUST NOT construct a second `&mut T` to the same processor (via
581/// `unsafe { &mut *STATIC.as_ptr() }`, transmute, or any other route)
582/// until the [`Graph`] has been dropped. Multiple `&` shared borrows
583/// across threads are also UB — `process` takes `&mut self`.
584///
585/// # Errors
586///
587/// Returns an error if the graph is full or not in `IDLE` state
588/// (i.e. [`Graph::build`] has already been called).
589pub fn graph_add_processor<T: AudioProcessor>(
590 graph: &mut bindings::ove_audio_graph,
591 processor: &'static mut T,
592 name: &[u8],
593) -> core::result::Result<i32, Error> {
594 unsafe extern "C" fn configure_trampoline(
595 _ctx: *mut c_void,
596 in_fmt: *const bindings::ove_audio_fmt,
597 out_fmt: *mut bindings::ove_audio_fmt,
598 ) -> core::ffi::c_int {
599 if !in_fmt.is_null() && !out_fmt.is_null() {
600 unsafe { *out_fmt = *in_fmt };
601 }
602 0
603 }
604
605 unsafe extern "C" fn process_trampoline<T: AudioProcessor>(
606 ctx: *mut c_void,
607 in_buf: *const bindings::ove_audio_buf,
608 out_buf: *mut bindings::ove_audio_buf,
609 ) -> core::ffi::c_int {
610 let proc_: &mut T = unsafe { &mut *(ctx as *mut T) };
611 let input = AudioBuf { raw: in_buf };
612 let output = AudioBuf { raw: out_buf };
613 proc_.process(&input, &output);
614 0
615 }
616
617 // Build the ops vtable as a helper struct with static lifetime.
618 // Function pointers are compile-time constants so this is safe.
619 struct OpsHolder<U: AudioProcessor>(core::marker::PhantomData<U>);
620 impl<U: AudioProcessor> OpsHolder<U> {
621 // SAFETY: All fields are Option<fn ptr> or None — this is a valid
622 // zero-initialization that we then patch with the real pointers.
623 // The struct is never mutated after creation.
624 #[allow(invalid_value)]
625 const OPS: bindings::ove_audio_node_ops = {
626 let mut ops: bindings::ove_audio_node_ops = unsafe { core::mem::zeroed() };
627 ops.configure = Some(configure_trampoline);
628 ops.process = Some(process_trampoline::<U>);
629 ops
630 };
631 }
632
633 let ctx = processor as *mut T as *mut c_void;
634 let rc = unsafe {
635 bindings::ove_audio_graph_add_node(
636 graph,
637 &OpsHolder::<T>::OPS,
638 ctx,
639 name.as_ptr() as *const _,
640 bindings::OVE_AUDIO_NODE_PROCESSOR,
641 )
642 };
643 if rc < 0 {
644 Err(Error::from_code(rc).unwrap_err())
645 } else {
646 Ok(rc)
647 }
648}
649
650/// A static wrapper for an [`AudioProcessor`] that provides safe
651/// `&'static mut` access for [`graph_add_processor`].
652///
653/// # Example
654///
655/// ```ignore
656/// static PROC: StaticProcessor<MyProc> = StaticProcessor::new(MyProc);
657/// let idx = graph_add_processor(&mut graph, PROC.get_mut(), b"my-proc\0")?;
658/// ```
659pub struct StaticProcessor<T> {
660 inner: core::cell::UnsafeCell<T>,
661}
662
663// SAFETY: `StaticProcessor<T>` is `UnsafeCell<T>` accessed only from the
664// audio thread (single-writer/single-reader by construction — the C side
665// owns the call site). The `T: Send` bound ensures `T` can cross from
666// the construction thread to the audio thread.
667unsafe impl<T: Send> Send for StaticProcessor<T> {}
668unsafe impl<T: Send> Sync for StaticProcessor<T> {}
669
670impl<T> StaticProcessor<T> {
671 /// Create a new static processor wrapper.
672 pub const fn new(val: T) -> Self {
673 Self {
674 inner: core::cell::UnsafeCell::new(val),
675 }
676 }
677
678 /// Obtain a `&'static mut` reference to the inner processor.
679 ///
680 /// Call once during graph setup before the audio thread starts.
681 #[allow(clippy::mut_from_ref)]
682 pub fn get_mut(&self) -> &mut T {
683 unsafe { &mut *self.inner.get() }
684 }
685}