Skip to content
File

Blob: firmware/crates/esp32-radio/src/platform/audio.rs

rust101 lines
1//! Single-owner native decoding/FFT. All native buffers stay on the C allocator.
2use super::ffi;
3use crate::error::{Error, Result, check};
4use radio_core::spectrum::{FFT_SIZE, HISTORY_FLOATS, STEREO_SAMPLES};
5use std::{ffi::c_void, marker::PhantomData, ptr::NonNull, rc::Rc, slice};
6 
7#[derive(Debug)]
8pub(crate) struct History {
9 pointer: NonNull<f32>,
10}
11impl History {
12 pub(crate) fn new() -> Result<Self> {
13 // SAFETY: C transfers a fresh, zeroed and float-aligned 4096-value PSRAM
14 // allocation. There are no native aliases or callbacks to its contents.
15 let pointer = NonNull::new(unsafe { ffi::radio_analysis_history_alloc() })
16 .ok_or(Error::new("analysis history allocation failed"))?;
17 Ok(Self { pointer })
18 }
19 pub(crate) fn as_mut(&mut self) -> &mut [f32] {
20 // SAFETY: exactly HISTORY_FLOATS initialized values in one allocation;
21 // exclusive borrow tied to self prevents aliases and use after Drop.
22 unsafe { slice::from_raw_parts_mut(self.pointer.as_ptr(), HISTORY_FLOATS) }
23 }
24}
25impl Drop for History {
26 fn drop(&mut self) {
27 // SAFETY: all borrows ended; release on the same allocator that created it.
28 unsafe { ffi::radio_analysis_history_free(self.pointer.as_ptr()) };
29 }
30}
31 
32/// Neither Send nor Sync: decoder, FFT and global DSP table lifetime share one task.
33#[derive(Debug)]
34pub(crate) struct Dsp {
35 handle: NonNull<c_void>,
36 fft: NonNull<f32>,
37 _task: PhantomData<Rc<()>>,
38}
39impl Dsp {
40 pub(crate) fn open() -> Result<Self> {
41 // SAFETY: scalar query of current FreeRTOS task. Check affinity before
42 // any Rust or native floating-point work can trigger lazy FPU pinning.
43 if unsafe { ffi::radio_current_core() } != 1 {
44 return Err(Error::new("analysis must run on core 1"));
45 }
46 // SAFETY: C enforces one DSP owner, retains configuration and allocates
47 // all buffers before returning. It registers no asynchronous callbacks.
48 let handle = NonNull::new(unsafe { ffi::radio_dsp_open() })
49 .ok_or(Error::new("audio analysis initialization failed"))?;
50 // SAFETY: a successfully opened context always contains its initialized,
51 // aligned 4096-float FFT buffer, owned until radio_dsp_free.
52 let fft = NonNull::new(unsafe { ffi::radio_dsp_fft_buffer(handle.as_ptr()) })
53 .expect("native DSP buffer invariant");
54 Ok(Self {
55 handle,
56 fft,
57 _task: PhantomData,
58 })
59 }
60 pub(crate) fn reset(&mut self) -> Result<()> {
61 // SAFETY: same-task sole owner and no native callbacks or outstanding PCM borrow.
62 let code = unsafe { ffi::radio_dsp_reset(self.handle.as_ptr()) };
63 check(code, "Opus decoder reset failed")
64 }
65 pub(crate) fn decode(&mut self, opus: &[u8]) -> Result<&[i16]> {
66 // SAFETY: C validates size and copies the packet into owned storage, then
67 // synchronously decodes and verifies the exact PCM size/format.
68 let code =
69 unsafe { ffi::radio_dsp_decode(self.handle.as_ptr(), opus.as_ptr(), opus.len()) };
70 check(code, "Opus decoding failed")?;
71 // SAFETY: successful decoding initialized exactly STEREO_SAMPLES values.
72 // Borrowing self prevents the next decode/reset/Drop while PCM is borrowed.
73 let pcm = unsafe { ffi::radio_dsp_pcm(self.handle.as_ptr()) };
74 // SAFETY: pcm is the live aligned native array described above, with no
75 // native writes until another exclusive method call after this borrow ends.
76 Ok(unsafe { slice::from_raw_parts(pcm, STEREO_SAMPLES) })
77 }
78 pub(crate) fn buffer_mut(&mut self) -> &mut [f32] {
79 // SAFETY: exclusive borrow of a fully initialized C-owned array; there
80 // are no asynchronous writers. It cannot coexist with transform or Drop.
81 unsafe { slice::from_raw_parts_mut(self.fft.as_ptr(), FFT_SIZE * 2) }
82 }
83 pub(crate) fn buffer(&self) -> &[f32] {
84 // SAFETY: same array as buffer_mut; self's shared borrow excludes mutation.
85 unsafe { slice::from_raw_parts(self.fft.as_ptr(), FFT_SIZE * 2) }
86 }
87 pub(crate) fn transform(&mut self) -> Result<()> {
88 // SAFETY: C transforms its own buffer synchronously, on its sole owning
89 // task, after all Rust buffer borrows have ended. Global tables have one owner.
90 let code = unsafe { ffi::radio_dsp_transform(self.handle.as_ptr()) };
91 check(code, "FFT failed")
92 }
93}
94impl Drop for Dsp {
95 fn drop(&mut self) {
96 // SAFETY: all synchronous operations and borrows ended; same task, sole
97 // owner. C closes the decoder and DSP tables before freeing its buffers.
98 unsafe { ffi::radio_dsp_free(self.handle.as_ptr()) };
99 }
100}