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/// Audio sample format.
18#[derive(Clone, Copy, PartialEq, Eq)]
19pub enum SampleFmt {
20    S16,
21    S32,
22    F32,
23}
24
25impl SampleFmt {
26    fn to_raw(self) -> u32 {
27        match self {
28            SampleFmt::S16 => 0,
29            SampleFmt::S32 => 1,
30            SampleFmt::F32 => 2,
31        }
32    }
33}
34
35/// Audio format descriptor.
36pub struct AudioFmt {
37    pub sample_rate: u32,
38    pub channels: u32,
39    pub sample_fmt: SampleFmt,
40}
41
42/// Initialize the audio graph.
43///
44/// # Errors
45/// Returns an error if `frames_per_period` is zero.
46pub fn graph_init(
47    graph: &mut bindings::ove_audio_graph,
48    frames_per_period: u32,
49) -> Result<()> {
50    let rc = unsafe { bindings::ove_audio_graph_init(graph, frames_per_period) };
51    Error::from_code(rc)
52}
53
54/// Tear down the graph and release all resources.
55pub fn graph_deinit(graph: &mut bindings::ove_audio_graph) {
56    unsafe { bindings::ove_audio_graph_deinit(graph) };
57}
58
59/// Add a node to the graph. Returns the node index.
60///
61/// # Errors
62/// Returns an error if the graph is full or not in IDLE state.
63pub fn graph_add_node(
64    graph: &mut bindings::ove_audio_graph,
65    ops: &bindings::ove_audio_node_ops,
66    ctx: *mut c_void,
67    name: &core::ffi::CStr,
68    node_type: bindings::ove_audio_node_type,
69) -> core::result::Result<i32, Error> {
70    let rc = unsafe {
71        bindings::ove_audio_graph_add_node(graph, ops, ctx, name.as_ptr(), node_type)
72    };
73    if rc < 0 {
74        Err(Error::from_code(rc).unwrap_err())
75    } else {
76        Ok(rc)
77    }
78}
79
80/// Connect two nodes. `from` feeds into `to`.
81///
82/// # Errors
83/// Returns an error on invalid indices or type violations.
84pub fn graph_connect(
85    graph: &mut bindings::ove_audio_graph,
86    from: u32,
87    to: u32,
88) -> Result<()> {
89    let rc = unsafe { bindings::ove_audio_graph_connect(graph, from, to) };
90    Error::from_code(rc)
91}
92
93/// Validate formats, resolve execution order, allocate buffers.
94///
95/// # Errors
96/// Returns an error on format mismatch, cycles, or OOM.
97pub fn graph_build(graph: &mut bindings::ove_audio_graph) -> Result<()> {
98    let rc = unsafe { bindings::ove_audio_graph_build(graph) };
99    Error::from_code(rc)
100}
101
102/// Start the graph (sink-driven mode).
103///
104/// # Errors
105/// Returns an error if graph is not in READY state.
106pub fn graph_start(graph: &mut bindings::ove_audio_graph) -> Result<()> {
107    let rc = unsafe { bindings::ove_audio_graph_start(graph) };
108    Error::from_code(rc)
109}
110
111/// Stop the graph.
112///
113/// # Errors
114/// Returns an error if graph is not in RUNNING state.
115pub fn graph_stop(graph: &mut bindings::ove_audio_graph) -> Result<()> {
116    let rc = unsafe { bindings::ove_audio_graph_stop(graph) };
117    Error::from_code(rc)
118}
119
120/// Process one cycle (app-driven mode).
121///
122/// # Errors
123/// Returns an error if graph is not built.
124pub fn graph_process(graph: &mut bindings::ove_audio_graph) -> Result<()> {
125    let rc = unsafe { bindings::ove_audio_graph_process(graph) };
126    Error::from_code(rc)
127}
128
129// ---------------------------------------------------------------------------
130// Audio device configuration builder
131// ---------------------------------------------------------------------------
132
133/// Build an `ove_audio_device_cfg` for I2S transport.
134///
135/// Constructs the C struct safely, including the transport-specific union
136/// fields, without requiring `unsafe` in application code.
137pub fn device_cfg_i2s(
138    sample_rate: u32,
139    channels: u32,
140    input_device: u32,
141) -> bindings::ove_audio_device_cfg {
142    let mut cfg: bindings::ove_audio_device_cfg = unsafe { core::mem::zeroed() };
143    cfg.transport = bindings::OVE_AUDIO_TRANSPORT_I2S;
144    cfg.fmt.sample_rate = sample_rate;
145    cfg.fmt.channels = channels;
146    cfg.fmt.sample_fmt = bindings::OVE_AUDIO_FMT_S16;
147    // Write i2s.input_device into the union via raw pointer
148    unsafe {
149        let union_ptr = &mut cfg as *mut _ as *mut u8;
150        let i2s_offset = core::mem::offset_of!(bindings::ove_audio_device_cfg, __bindgen_anon_1);
151        let i2s_ptr = union_ptr.add(i2s_offset) as *mut u32;
152        *i2s_ptr = input_device;
153    }
154    cfg
155}
156
157// ---------------------------------------------------------------------------
158// Safe Graph wrapper
159// ---------------------------------------------------------------------------
160
161/// Owned audio graph session.
162///
163/// Wraps the C `ove_audio_graph` in a safe API.  All `unsafe` FFI calls
164/// are encapsulated — application code uses only safe methods.
165///
166/// # Example
167///
168/// ```ignore
169/// let mut g = Graph::new(512)?;
170/// let dev = device_cfg_i2s(16000, 1, 1);
171/// let src  = g.device_source(&dev, b"mic\0")?;
172/// let proc = g.add_processor(PROC.get_mut(), b"dsp\0")?;
173/// let sink = g.device_sink(&dev, b"spk\0")?;
174/// g.connect(src, proc)?;
175/// g.connect(proc, sink)?;
176/// g.build()?;
177/// g.start()?;
178/// ```
179pub struct Graph {
180    inner: bindings::ove_audio_graph,
181}
182
183impl Graph {
184    /// Create and initialize a new audio graph.
185    pub fn new(frames_per_period: u32) -> Result<Self> {
186        let mut inner: bindings::ove_audio_graph = unsafe { core::mem::zeroed() };
187        let rc = unsafe { bindings::ove_audio_graph_init(&mut inner, frames_per_period) };
188        Error::from_code(rc)?;
189        Ok(Self { inner })
190    }
191
192    /// Add a hardware audio source node.  Returns the node index.
193    pub fn device_source(
194        &mut self,
195        cfg: &bindings::ove_audio_device_cfg,
196        name: &[u8],
197    ) -> core::result::Result<u32, Error> {
198        let rc = unsafe {
199            bindings::ove_audio_device_source(
200                &mut self.inner,
201                cfg,
202                name.as_ptr() as *const _,
203            )
204        };
205        if rc < 0 {
206            Err(Error::from_code(rc).unwrap_err())
207        } else {
208            Ok(rc as u32)
209        }
210    }
211
212    /// Add a hardware audio sink node.  Returns the node index.
213    pub fn device_sink(
214        &mut self,
215        cfg: &bindings::ove_audio_device_cfg,
216        name: &[u8],
217    ) -> core::result::Result<u32, Error> {
218        let rc = unsafe {
219            bindings::ove_audio_device_sink(
220                &mut self.inner,
221                cfg,
222                name.as_ptr() as *const _,
223            )
224        };
225        if rc < 0 {
226            Err(Error::from_code(rc).unwrap_err())
227        } else {
228            Ok(rc as u32)
229        }
230    }
231
232    /// Register a custom processor node.  Returns the node index.
233    pub fn add_processor<T: AudioProcessor>(
234        &mut self,
235        processor: &'static mut T,
236        name: &[u8],
237    ) -> core::result::Result<u32, Error> {
238        graph_add_processor(&mut self.inner, processor, name)
239            .map(|i| i as u32)
240    }
241
242    /// Connect two nodes.  `from` feeds into `to`.
243    pub fn connect(&mut self, from: u32, to: u32) -> Result<()> {
244        graph_connect(&mut self.inner, from, to)
245    }
246
247    /// Validate formats, resolve execution order, allocate buffers.
248    pub fn build(&mut self) -> Result<()> {
249        graph_build(&mut self.inner)
250    }
251
252    /// Start the graph (sink-driven mode).
253    pub fn start(&mut self) -> Result<()> {
254        graph_start(&mut self.inner)
255    }
256
257    /// Stop the graph.
258    pub fn stop(&mut self) -> Result<()> {
259        graph_stop(&mut self.inner)
260    }
261
262    /// Process one cycle (app-driven mode).
263    pub fn process(&mut self) -> Result<()> {
264        graph_process(&mut self.inner)
265    }
266}
267
268impl Drop for Graph {
269    fn drop(&mut self) {
270        graph_deinit(&mut self.inner);
271    }
272}
273
274// ---------------------------------------------------------------------------
275// Safe audio processor trait
276// ---------------------------------------------------------------------------
277
278/// Audio buffer descriptor (borrowed from the C layer).
279pub struct AudioBuf {
280    raw: *const bindings::ove_audio_buf,
281}
282
283impl AudioBuf {
284    /// Get a slice of interleaved S16 samples.
285    pub fn data_s16(&self) -> &[i16] {
286        unsafe {
287            let buf = &*self.raw;
288            let count = buf.frames as usize * (*buf.fmt).channels as usize;
289            core::slice::from_raw_parts(buf.data as *const i16, count)
290        }
291    }
292
293    /// Get a mutable slice of interleaved S16 samples.
294    pub fn data_s16_mut(&self) -> &mut [i16] {
295        unsafe {
296            let buf = &*self.raw;
297            let count = buf.frames as usize * (*buf.fmt).channels as usize;
298            core::slice::from_raw_parts_mut(buf.data as *mut i16, count)
299        }
300    }
301
302    /// Number of frames in this buffer.
303    pub fn frames(&self) -> u32 {
304        unsafe { (*self.raw).frames }
305    }
306}
307
308/// Trait for implementing custom audio processing nodes.
309///
310/// All FFI bridging is handled by the binding layer — implement this
311/// trait on a plain Rust struct with no `unsafe` or `extern "C"`.
312///
313/// # Example
314/// ```ignore
315/// struct MyProcessor { ... }
316/// impl ove::audio::AudioProcessor for MyProcessor {
317///     fn process(&mut self, input: &AudioBuf, output: &AudioBuf) {
318///         let src = input.data_s16();
319///         let dst = output.data_s16_mut();
320///         dst.copy_from_slice(src); // passthrough
321///     }
322/// }
323/// ```
324pub trait AudioProcessor {
325    /// Process one audio period. Called from the audio thread.
326    fn process(&mut self, input: &AudioBuf, output: &AudioBuf);
327}
328
329/// Register a custom processor node on the graph.
330///
331/// The processor must live in a `&'static mut` location (e.g. a
332/// `StaticCell` or `static mut`).  All FFI trampolines are generated
333/// internally — no `unsafe` or `extern "C"` needed in application code.
334///
335/// # Errors
336/// Returns an error if the graph is full or not in IDLE state.
337pub fn graph_add_processor<T: AudioProcessor>(
338    graph: &mut bindings::ove_audio_graph,
339    processor: &'static mut T,
340    name: &[u8],
341) -> core::result::Result<i32, Error> {
342    unsafe extern "C" fn configure_trampoline(
343        _ctx: *mut c_void,
344        in_fmt: *const bindings::ove_audio_fmt,
345        out_fmt: *mut bindings::ove_audio_fmt,
346    ) -> core::ffi::c_int {
347        if !in_fmt.is_null() && !out_fmt.is_null() {
348            *out_fmt = *in_fmt;
349        }
350        0
351    }
352
353    unsafe extern "C" fn process_trampoline<T: AudioProcessor>(
354        ctx: *mut c_void,
355        in_buf: *const bindings::ove_audio_buf,
356        out_buf: *mut bindings::ove_audio_buf,
357    ) -> core::ffi::c_int {
358        let proc_: &mut T = &mut *(ctx as *mut T);
359        let input = AudioBuf { raw: in_buf };
360        let output = AudioBuf { raw: out_buf };
361        proc_.process(&input, &output);
362        0
363    }
364
365    // Build the ops vtable as a helper struct with static lifetime.
366    // Function pointers are compile-time constants so this is safe.
367    struct OpsHolder<U: AudioProcessor>(core::marker::PhantomData<U>);
368    impl<U: AudioProcessor> OpsHolder<U> {
369        // SAFETY: All fields are Option<fn ptr> or None — this is a valid
370        // zero-initialization that we then patch with the real pointers.
371        // The struct is never mutated after creation.
372        #[allow(invalid_value)]
373        const OPS: bindings::ove_audio_node_ops = {
374            let mut ops: bindings::ove_audio_node_ops = unsafe { core::mem::zeroed() };
375            ops.configure = Some(configure_trampoline);
376            ops.process = Some(process_trampoline::<U>);
377            ops
378        };
379    }
380
381    let ctx = processor as *mut T as *mut c_void;
382    let rc = unsafe {
383        bindings::ove_audio_graph_add_node(
384            graph,
385            &OpsHolder::<T>::OPS,
386            ctx,
387            name.as_ptr() as *const _,
388            bindings::OVE_AUDIO_NODE_PROCESSOR,
389        )
390    };
391    if rc < 0 {
392        Err(Error::from_code(rc).unwrap_err())
393    } else {
394        Ok(rc)
395    }
396}
397
398/// A static wrapper for an [`AudioProcessor`] that provides safe
399/// `&'static mut` access for [`graph_add_processor`].
400///
401/// # Example
402///
403/// ```ignore
404/// static PROC: StaticProcessor<MyProc> = StaticProcessor::new(MyProc);
405/// let idx = graph_add_processor(&mut graph, PROC.get_mut(), b"my-proc\0")?;
406/// ```
407pub struct StaticProcessor<T> {
408    inner: core::cell::UnsafeCell<T>,
409}
410
411unsafe impl<T: Send> Send for StaticProcessor<T> {}
412unsafe impl<T: Send> Sync for StaticProcessor<T> {}
413
414impl<T> StaticProcessor<T> {
415    /// Create a new static processor wrapper.
416    pub const fn new(val: T) -> Self {
417        Self {
418            inner: core::cell::UnsafeCell::new(val),
419        }
420    }
421
422    /// Obtain a `&'static mut` reference to the inner processor.
423    ///
424    /// Call once during graph setup before the audio thread starts.
425    #[allow(clippy::mut_from_ref)]
426    pub fn get_mut(&self) -> &mut T {
427        unsafe { &mut *self.inner.get() }
428    }
429}