Skip to main content

wavecraft_dev_server/audio/
ffi_processor.rs

1//! FFI processor wrapper and dev audio processor trait.
2//!
3//! This module bridges the C-ABI `DevProcessorVTable` (loaded from the user's
4//! cdylib) to a safe Rust trait that the audio server can drive.
5
6use std::ffi::c_void;
7use wavecraft_protocol::DevProcessorVTable;
8
9/// Simplified audio processor trait for dev mode.
10///
11/// Unlike the full `wavecraft_dsp::Processor` trait, this has no associated
12/// types and works with both direct Rust implementations and FFI-loaded
13/// processors via type erasure.
14pub trait DevAudioProcessor: Send + 'static {
15    /// Process deinterleaved audio in-place.
16    fn process(&mut self, channels: &mut [&mut [f32]]);
17
18    /// Update the processor's sample rate.
19    fn set_sample_rate(&mut self, sample_rate: f32);
20
21    /// Reset processor state.
22    fn reset(&mut self);
23}
24
25/// Wraps a `DevProcessorVTable` into a safe `DevAudioProcessor`.
26///
27/// Owns the opaque processor instance and dispatches through vtable
28/// function pointers. All allocation and deallocation happens inside
29/// the dylib via the vtable — no cross-allocator issues.
30pub struct FfiProcessor {
31    instance: *mut c_void,
32    vtable: DevProcessorVTable,
33}
34
35// SAFETY: The processor instance is only accessed from the cpal audio
36// callback thread (single-threaded access). The `Send` bound allows
37// transferring it from the main thread (where it's created) to the
38// audio thread. `FfiProcessor` is NOT `Sync` — no concurrent access.
39unsafe impl Send for FfiProcessor {}
40
41impl FfiProcessor {
42    /// Create a new FFI processor from a loaded vtable.
43    ///
44    /// Calls the vtable's `create` function to allocate the processor
45    /// inside the dylib. Returns `None` if `create` returns null
46    /// (indicating a panic or allocation failure inside the dylib).
47    pub fn new(vtable: &DevProcessorVTable) -> Option<Self> {
48        let instance = (vtable.create)();
49        if instance.is_null() {
50            return None;
51        }
52        Some(Self {
53            instance,
54            vtable: *vtable,
55        })
56    }
57
58    fn process_dimensions(channels: &[&mut [f32]]) -> Option<(u32, u32)> {
59        let num_channels = channels.len() as u32;
60        if num_channels == 0 || channels[0].is_empty() {
61            return None;
62        }
63
64        Some((num_channels, channels[0].len() as u32))
65    }
66
67    fn prepare_channel_ptrs(channels: &mut [&mut [f32]]) -> Option<[*mut f32; 2]> {
68        // Real-time safety: use a stack-allocated array instead of Vec.
69        // Wavecraft targets stereo (2 channels). Guard against unexpected
70        // multi-channel input to avoid out-of-bounds access.
71        if channels.len() > 2 {
72            tracing::error!(
73                num_channels = channels.len(),
74                "FfiProcessor::process() received more than 2 channels; skipping"
75            );
76            return None;
77        }
78
79        // Build fixed-size array of channel pointers for the C-ABI call.
80        // No heap allocation — this lives on the stack.
81        let mut ptrs: [*mut f32; 2] = [std::ptr::null_mut(); 2];
82        for (index, channel) in channels.iter_mut().enumerate() {
83            ptrs[index] = channel.as_mut_ptr();
84        }
85
86        Some(ptrs)
87    }
88}
89
90impl DevAudioProcessor for FfiProcessor {
91    fn process(&mut self, channels: &mut [&mut [f32]]) {
92        let Some((num_channels, num_samples)) = Self::process_dimensions(channels) else {
93            return;
94        };
95
96        debug_assert!(
97            !self.instance.is_null(),
98            "FFI processor instance should be valid"
99        );
100        debug_assert!(
101            channels
102                .iter()
103                .all(|channel| channel.len() == num_samples as usize),
104            "FFI processor expects channel slices with equal lengths"
105        );
106
107        let Some(mut ptrs) = Self::prepare_channel_ptrs(channels) else {
108            return;
109        };
110
111        (self.vtable.process)(self.instance, ptrs.as_mut_ptr(), num_channels, num_samples);
112    }
113
114    fn set_sample_rate(&mut self, sample_rate: f32) {
115        (self.vtable.set_sample_rate)(self.instance, sample_rate);
116    }
117
118    fn reset(&mut self) {
119        (self.vtable.reset)(self.instance);
120    }
121}
122
123impl Drop for FfiProcessor {
124    fn drop(&mut self) {
125        if !self.instance.is_null() {
126            (self.vtable.drop)(self.instance);
127            self.instance = std::ptr::null_mut();
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use std::sync::Mutex;
136    use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
137
138    // Mutex to serialize tests that share static mock flags.
139    // This prevents race conditions when tests run in parallel.
140    static TEST_LOCK: Mutex<()> = Mutex::new(());
141
142    // Static flags for mock vtable functions
143    static CREATE_CALLED: AtomicBool = AtomicBool::new(false);
144    static PROCESS_CALLED: AtomicBool = AtomicBool::new(false);
145    static SET_SAMPLE_RATE_CALLED: AtomicBool = AtomicBool::new(false);
146    static RESET_CALLED: AtomicBool = AtomicBool::new(false);
147    static DROP_CALLED: AtomicBool = AtomicBool::new(false);
148    static PROCESS_CHANNELS: AtomicU32 = AtomicU32::new(0);
149    static PROCESS_SAMPLES: AtomicU32 = AtomicU32::new(0);
150
151    fn reset_flags() {
152        CREATE_CALLED.store(false, Ordering::SeqCst);
153        PROCESS_CALLED.store(false, Ordering::SeqCst);
154        SET_SAMPLE_RATE_CALLED.store(false, Ordering::SeqCst);
155        RESET_CALLED.store(false, Ordering::SeqCst);
156        DROP_CALLED.store(false, Ordering::SeqCst);
157        PROCESS_CHANNELS.store(0, Ordering::SeqCst);
158        PROCESS_SAMPLES.store(0, Ordering::SeqCst);
159    }
160
161    extern "C" fn mock_create() -> *mut c_void {
162        CREATE_CALLED.store(true, Ordering::SeqCst);
163        // Return a non-null sentinel (we never dereference it in mock)
164        std::ptr::dangling_mut::<c_void>()
165    }
166
167    extern "C" fn mock_create_null() -> *mut c_void {
168        CREATE_CALLED.store(true, Ordering::SeqCst);
169        std::ptr::null_mut()
170    }
171
172    extern "C" fn mock_process(
173        _instance: *mut c_void,
174        _channels: *mut *mut f32,
175        num_channels: u32,
176        num_samples: u32,
177    ) {
178        PROCESS_CALLED.store(true, Ordering::SeqCst);
179        PROCESS_CHANNELS.store(num_channels, Ordering::SeqCst);
180        PROCESS_SAMPLES.store(num_samples, Ordering::SeqCst);
181    }
182
183    extern "C" fn mock_set_sample_rate(_instance: *mut c_void, _sample_rate: f32) {
184        SET_SAMPLE_RATE_CALLED.store(true, Ordering::SeqCst);
185    }
186
187    extern "C" fn mock_reset(_instance: *mut c_void) {
188        RESET_CALLED.store(true, Ordering::SeqCst);
189    }
190
191    extern "C" fn mock_drop(_instance: *mut c_void) {
192        DROP_CALLED.store(true, Ordering::SeqCst);
193    }
194
195    fn mock_vtable() -> DevProcessorVTable {
196        DevProcessorVTable {
197            version: wavecraft_protocol::DEV_PROCESSOR_VTABLE_VERSION,
198            create: mock_create,
199            process: mock_process,
200            set_sample_rate: mock_set_sample_rate,
201            reset: mock_reset,
202            drop: mock_drop,
203        }
204    }
205
206    #[test]
207    fn test_ffi_processor_lifecycle() {
208        let _guard = TEST_LOCK.lock().unwrap();
209        reset_flags();
210        let vtable = mock_vtable();
211
212        let mut processor = FfiProcessor::new(&vtable).expect("create should succeed");
213        assert!(CREATE_CALLED.load(Ordering::SeqCst));
214
215        // Process some audio
216        let mut left = vec![0.0f32; 128];
217        let mut right = vec![0.0f32; 128];
218        let mut channels: Vec<&mut [f32]> = vec![&mut left, &mut right];
219        processor.process(&mut channels);
220        assert!(PROCESS_CALLED.load(Ordering::SeqCst));
221        assert_eq!(PROCESS_CHANNELS.load(Ordering::SeqCst), 2);
222        assert_eq!(PROCESS_SAMPLES.load(Ordering::SeqCst), 128);
223
224        // Drop should call vtable.drop
225        drop(processor);
226        assert!(DROP_CALLED.load(Ordering::SeqCst));
227    }
228
229    #[test]
230    fn test_ffi_processor_set_sample_rate_and_reset() {
231        let _guard = TEST_LOCK.lock().unwrap();
232        reset_flags();
233        let vtable = mock_vtable();
234
235        let mut processor = FfiProcessor::new(&vtable).expect("create should succeed");
236
237        processor.set_sample_rate(48000.0);
238        assert!(SET_SAMPLE_RATE_CALLED.load(Ordering::SeqCst));
239
240        processor.reset();
241        assert!(RESET_CALLED.load(Ordering::SeqCst));
242
243        drop(processor);
244    }
245
246    #[test]
247    fn test_ffi_processor_null_create_returns_none() {
248        let _guard = TEST_LOCK.lock().unwrap();
249        reset_flags();
250        let mut vtable = mock_vtable();
251        vtable.create = mock_create_null;
252
253        let result = FfiProcessor::new(&vtable);
254        assert!(CREATE_CALLED.load(Ordering::SeqCst));
255        assert!(
256            result.is_none(),
257            "Should return None when create returns null"
258        );
259    }
260
261    #[test]
262    fn test_ffi_processor_empty_channels_noop() {
263        let _guard = TEST_LOCK.lock().unwrap();
264        reset_flags();
265        let vtable = mock_vtable();
266        let mut processor = FfiProcessor::new(&vtable).expect("create should succeed");
267
268        // Empty channels → should not call process
269        PROCESS_CALLED.store(false, Ordering::SeqCst);
270        let mut channels: Vec<&mut [f32]> = vec![];
271        processor.process(&mut channels);
272        assert!(
273            !PROCESS_CALLED.load(Ordering::SeqCst),
274            "Should not call vtable.process with empty channels"
275        );
276
277        drop(processor);
278    }
279}