wavecraft_dev_server/audio/
ffi_processor.rs1use std::ffi::c_void;
7use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
8use wavecraft_protocol::DevProcessorVTable;
9
10pub trait DevAudioProcessor: Send + 'static {
16 fn process(&mut self, channels: &mut [&mut [f32]]);
18
19 fn apply_plain_values(&mut self, values: &[f32]);
21
22 fn set_sample_rate(&mut self, sample_rate: f32);
24
25 fn reset(&mut self);
27}
28
29pub struct FfiProcessor {
35 instance: *mut c_void,
36 vtable: DevProcessorVTable,
37 supports_plain_values: bool,
38 unsupported_channel_count: AtomicU32,
39 unsupported_channel_flag: AtomicBool,
40}
41
42unsafe impl Send for FfiProcessor {}
47
48impl FfiProcessor {
49 pub fn new(vtable: &DevProcessorVTable) -> Option<Self> {
55 let instance = (vtable.create)();
56 if instance.is_null() {
57 return None;
58 }
59 Some(Self {
60 instance,
61 vtable: *vtable,
62 supports_plain_values: vtable.version >= 2,
63 unsupported_channel_count: AtomicU32::new(0),
64 unsupported_channel_flag: AtomicBool::new(false),
65 })
66 }
67
68 fn process_dimensions(channels: &[&mut [f32]]) -> Option<(u32, u32)> {
69 let num_channels = channels.len() as u32;
70 if num_channels == 0 || channels[0].is_empty() {
71 return None;
72 }
73
74 Some((num_channels, channels[0].len() as u32))
75 }
76
77 fn prepare_channel_ptrs(&self, channels: &mut [&mut [f32]]) -> Option<[*mut f32; 2]> {
78 if channels.len() > 2 {
82 self.unsupported_channel_count
86 .fetch_add(1, Ordering::Relaxed);
87 self.unsupported_channel_flag.store(true, Ordering::Relaxed);
88 return None;
89 }
90
91 let mut ptrs: [*mut f32; 2] = [std::ptr::null_mut(); 2];
94 for (index, channel) in channels.iter_mut().enumerate() {
95 ptrs[index] = channel.as_mut_ptr();
96 }
97
98 Some(ptrs)
99 }
100
101 pub fn take_unsupported_channel_count(&self) -> u32 {
104 self.unsupported_channel_count.swap(0, Ordering::Relaxed)
105 }
106
107 pub fn take_unsupported_channel_flag(&self) -> bool {
110 self.unsupported_channel_flag.swap(false, Ordering::Relaxed)
111 }
112}
113
114impl DevAudioProcessor for FfiProcessor {
115 fn process(&mut self, channels: &mut [&mut [f32]]) {
116 let Some((num_channels, num_samples)) = Self::process_dimensions(channels) else {
117 return;
118 };
119
120 debug_assert!(
121 !self.instance.is_null(),
122 "FFI processor instance should be valid"
123 );
124 debug_assert!(
125 channels
126 .iter()
127 .all(|channel| channel.len() == num_samples as usize),
128 "FFI processor expects channel slices with equal lengths"
129 );
130
131 let Some(mut ptrs) = self.prepare_channel_ptrs(channels) else {
132 return;
133 };
134
135 (self.vtable.process)(self.instance, ptrs.as_mut_ptr(), num_channels, num_samples);
136 }
137
138 fn apply_plain_values(&mut self, values: &[f32]) {
139 if !self.supports_plain_values {
140 return;
141 }
142
143 unsafe {
147 (self.vtable.apply_plain_values)(self.instance, values.as_ptr(), values.len());
148 }
149 }
150
151 fn set_sample_rate(&mut self, sample_rate: f32) {
152 (self.vtable.set_sample_rate)(self.instance, sample_rate);
153 }
154
155 fn reset(&mut self) {
156 (self.vtable.reset)(self.instance);
157 }
158}
159
160impl Drop for FfiProcessor {
161 fn drop(&mut self) {
162 if !self.instance.is_null() {
163 (self.vtable.drop)(self.instance);
164 self.instance = std::ptr::null_mut();
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use std::sync::Mutex;
173
174 static TEST_LOCK: Mutex<()> = Mutex::new(());
177
178 static CREATE_CALLED: AtomicBool = AtomicBool::new(false);
180 static PROCESS_CALLED: AtomicBool = AtomicBool::new(false);
181 static SET_SAMPLE_RATE_CALLED: AtomicBool = AtomicBool::new(false);
182 static RESET_CALLED: AtomicBool = AtomicBool::new(false);
183 static DROP_CALLED: AtomicBool = AtomicBool::new(false);
184 static APPLY_PLAIN_VALUES_CALLED: AtomicBool = AtomicBool::new(false);
185 static APPLY_PLAIN_VALUES_LEN: AtomicU32 = AtomicU32::new(0);
186 static PROCESS_CHANNELS: AtomicU32 = AtomicU32::new(0);
187 static PROCESS_SAMPLES: AtomicU32 = AtomicU32::new(0);
188
189 fn reset_flags() {
190 CREATE_CALLED.store(false, Ordering::SeqCst);
191 PROCESS_CALLED.store(false, Ordering::SeqCst);
192 SET_SAMPLE_RATE_CALLED.store(false, Ordering::SeqCst);
193 RESET_CALLED.store(false, Ordering::SeqCst);
194 DROP_CALLED.store(false, Ordering::SeqCst);
195 APPLY_PLAIN_VALUES_CALLED.store(false, Ordering::SeqCst);
196 APPLY_PLAIN_VALUES_LEN.store(0, Ordering::SeqCst);
197 PROCESS_CHANNELS.store(0, Ordering::SeqCst);
198 PROCESS_SAMPLES.store(0, Ordering::SeqCst);
199 }
200
201 extern "C" fn mock_create() -> *mut c_void {
202 CREATE_CALLED.store(true, Ordering::SeqCst);
203 std::ptr::dangling_mut::<c_void>()
205 }
206
207 extern "C" fn mock_create_null() -> *mut c_void {
208 CREATE_CALLED.store(true, Ordering::SeqCst);
209 std::ptr::null_mut()
210 }
211
212 extern "C" fn mock_process(
213 _instance: *mut c_void,
214 _channels: *mut *mut f32,
215 num_channels: u32,
216 num_samples: u32,
217 ) {
218 PROCESS_CALLED.store(true, Ordering::SeqCst);
219 PROCESS_CHANNELS.store(num_channels, Ordering::SeqCst);
220 PROCESS_SAMPLES.store(num_samples, Ordering::SeqCst);
221 }
222
223 extern "C" fn mock_set_sample_rate(_instance: *mut c_void, _sample_rate: f32) {
224 SET_SAMPLE_RATE_CALLED.store(true, Ordering::SeqCst);
225 }
226
227 extern "C" fn mock_reset(_instance: *mut c_void) {
228 RESET_CALLED.store(true, Ordering::SeqCst);
229 }
230
231 extern "C" fn mock_drop(_instance: *mut c_void) {
232 DROP_CALLED.store(true, Ordering::SeqCst);
233 }
234
235 unsafe extern "C" fn mock_apply_plain_values(
236 _instance: *mut c_void,
237 _values_ptr: *const f32,
238 len: usize,
239 ) {
240 APPLY_PLAIN_VALUES_CALLED.store(true, Ordering::SeqCst);
241 APPLY_PLAIN_VALUES_LEN.store(len as u32, Ordering::SeqCst);
242 }
243
244 fn mock_vtable() -> DevProcessorVTable {
245 DevProcessorVTable {
246 version: wavecraft_protocol::DEV_PROCESSOR_VTABLE_VERSION,
247 create: mock_create,
248 process: mock_process,
249 apply_plain_values: mock_apply_plain_values,
250 set_sample_rate: mock_set_sample_rate,
251 reset: mock_reset,
252 drop: mock_drop,
253 }
254 }
255
256 #[test]
257 fn test_ffi_processor_lifecycle() {
258 let _guard = TEST_LOCK.lock().unwrap();
259 reset_flags();
260 let vtable = mock_vtable();
261
262 let mut processor = FfiProcessor::new(&vtable).expect("create should succeed");
263 assert!(CREATE_CALLED.load(Ordering::SeqCst));
264
265 let mut left = vec![0.0f32; 128];
267 let mut right = vec![0.0f32; 128];
268 let mut channels: Vec<&mut [f32]> = vec![&mut left, &mut right];
269 processor.process(&mut channels);
270 assert!(PROCESS_CALLED.load(Ordering::SeqCst));
271 assert_eq!(PROCESS_CHANNELS.load(Ordering::SeqCst), 2);
272 assert_eq!(PROCESS_SAMPLES.load(Ordering::SeqCst), 128);
273
274 drop(processor);
276 assert!(DROP_CALLED.load(Ordering::SeqCst));
277 }
278
279 #[test]
280 fn test_ffi_processor_set_sample_rate_and_reset() {
281 let _guard = TEST_LOCK.lock().unwrap();
282 reset_flags();
283 let vtable = mock_vtable();
284
285 let mut processor = FfiProcessor::new(&vtable).expect("create should succeed");
286
287 processor.set_sample_rate(48000.0);
288 assert!(SET_SAMPLE_RATE_CALLED.load(Ordering::SeqCst));
289
290 processor.reset();
291 assert!(RESET_CALLED.load(Ordering::SeqCst));
292
293 drop(processor);
294 }
295
296 #[test]
297 fn test_ffi_processor_apply_plain_values() {
298 let _guard = TEST_LOCK.lock().unwrap();
299 reset_flags();
300 let vtable = mock_vtable();
301
302 let mut processor = FfiProcessor::new(&vtable).expect("create should succeed");
303 processor.apply_plain_values(&[0.1, 0.2, 0.3]);
304
305 assert!(APPLY_PLAIN_VALUES_CALLED.load(Ordering::SeqCst));
306 assert_eq!(APPLY_PLAIN_VALUES_LEN.load(Ordering::SeqCst), 3);
307 }
308
309 #[test]
310 fn test_ffi_processor_null_create_returns_none() {
311 let _guard = TEST_LOCK.lock().unwrap();
312 reset_flags();
313 let mut vtable = mock_vtable();
314 vtable.create = mock_create_null;
315
316 let result = FfiProcessor::new(&vtable);
317 assert!(CREATE_CALLED.load(Ordering::SeqCst));
318 assert!(
319 result.is_none(),
320 "Should return None when create returns null"
321 );
322 }
323
324 #[test]
325 fn test_ffi_processor_empty_channels_noop() {
326 let _guard = TEST_LOCK.lock().unwrap();
327 reset_flags();
328 let vtable = mock_vtable();
329 let mut processor = FfiProcessor::new(&vtable).expect("create should succeed");
330
331 PROCESS_CALLED.store(false, Ordering::SeqCst);
333 let mut channels: Vec<&mut [f32]> = vec![];
334 processor.process(&mut channels);
335 assert!(
336 !PROCESS_CALLED.load(Ordering::SeqCst),
337 "Should not call vtable.process with empty channels"
338 );
339
340 drop(processor);
341 }
342
343 #[test]
344 fn test_ffi_processor_multichannel_records_rt_safe_diagnostic() {
345 let _guard = TEST_LOCK.lock().unwrap();
346 reset_flags();
347 let vtable = mock_vtable();
348 let mut processor = FfiProcessor::new(&vtable).expect("create should succeed");
349
350 PROCESS_CALLED.store(false, Ordering::SeqCst);
352 let mut ch1 = vec![0.0f32; 16];
353 let mut ch2 = vec![0.0f32; 16];
354 let mut ch3 = vec![0.0f32; 16];
355 let mut channels: Vec<&mut [f32]> = vec![&mut ch1, &mut ch2, &mut ch3];
356
357 processor.process(&mut channels);
358
359 assert!(
360 !PROCESS_CALLED.load(Ordering::SeqCst),
361 "Should not call vtable.process when channel count > 2"
362 );
363 assert!(processor.take_unsupported_channel_flag());
364 assert_eq!(processor.take_unsupported_channel_count(), 1);
365
366 assert!(!processor.take_unsupported_channel_flag());
368 assert_eq!(processor.take_unsupported_channel_count(), 0);
369
370 drop(processor);
371 }
372}