wavecraft_dev_server/audio/
ffi_processor.rs1use std::ffi::c_void;
7use wavecraft_protocol::DevProcessorVTable;
8
9pub trait DevAudioProcessor: Send + 'static {
15 fn process(&mut self, channels: &mut [&mut [f32]]);
17
18 fn set_sample_rate(&mut self, sample_rate: f32);
20
21 fn reset(&mut self);
23}
24
25pub struct FfiProcessor {
31 instance: *mut c_void,
32 vtable: DevProcessorVTable,
33}
34
35unsafe impl Send for FfiProcessor {}
40
41impl FfiProcessor {
42 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 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 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 static TEST_LOCK: Mutex<()> = Mutex::new(());
141
142 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 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 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(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 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}