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
59impl DevAudioProcessor for FfiProcessor {
60 fn process(&mut self, channels: &mut [&mut [f32]]) {
61 let num_channels = channels.len() as u32;
62 if num_channels == 0 || channels[0].is_empty() {
63 return;
64 }
65 let num_samples = channels[0].len() as u32;
66
67 if channels.len() > 2 {
71 tracing::error!(
72 num_channels = channels.len(),
73 "FfiProcessor::process() received more than 2 channels; skipping"
74 );
75 return;
76 }
77
78 let mut ptrs: [*mut f32; 2] = [std::ptr::null_mut(); 2];
81 for (i, ch) in channels.iter_mut().enumerate() {
82 ptrs[i] = ch.as_mut_ptr();
83 }
84
85 (self.vtable.process)(self.instance, ptrs.as_mut_ptr(), num_channels, num_samples);
86 }
87
88 fn set_sample_rate(&mut self, sample_rate: f32) {
89 (self.vtable.set_sample_rate)(self.instance, sample_rate);
90 }
91
92 fn reset(&mut self) {
93 (self.vtable.reset)(self.instance);
94 }
95}
96
97impl Drop for FfiProcessor {
98 fn drop(&mut self) {
99 if !self.instance.is_null() {
100 (self.vtable.drop)(self.instance);
101 self.instance = std::ptr::null_mut();
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
110 use std::sync::Mutex;
111
112 static TEST_LOCK: Mutex<()> = Mutex::new(());
115
116 static CREATE_CALLED: AtomicBool = AtomicBool::new(false);
118 static PROCESS_CALLED: AtomicBool = AtomicBool::new(false);
119 static SET_SAMPLE_RATE_CALLED: AtomicBool = AtomicBool::new(false);
120 static RESET_CALLED: AtomicBool = AtomicBool::new(false);
121 static DROP_CALLED: AtomicBool = AtomicBool::new(false);
122 static PROCESS_CHANNELS: AtomicU32 = AtomicU32::new(0);
123 static PROCESS_SAMPLES: AtomicU32 = AtomicU32::new(0);
124
125 fn reset_flags() {
126 CREATE_CALLED.store(false, Ordering::SeqCst);
127 PROCESS_CALLED.store(false, Ordering::SeqCst);
128 SET_SAMPLE_RATE_CALLED.store(false, Ordering::SeqCst);
129 RESET_CALLED.store(false, Ordering::SeqCst);
130 DROP_CALLED.store(false, Ordering::SeqCst);
131 PROCESS_CHANNELS.store(0, Ordering::SeqCst);
132 PROCESS_SAMPLES.store(0, Ordering::SeqCst);
133 }
134
135 extern "C" fn mock_create() -> *mut c_void {
136 CREATE_CALLED.store(true, Ordering::SeqCst);
137 std::ptr::dangling_mut::<c_void>()
139 }
140
141 extern "C" fn mock_create_null() -> *mut c_void {
142 CREATE_CALLED.store(true, Ordering::SeqCst);
143 std::ptr::null_mut()
144 }
145
146 extern "C" fn mock_process(
147 _instance: *mut c_void,
148 _channels: *mut *mut f32,
149 num_channels: u32,
150 num_samples: u32,
151 ) {
152 PROCESS_CALLED.store(true, Ordering::SeqCst);
153 PROCESS_CHANNELS.store(num_channels, Ordering::SeqCst);
154 PROCESS_SAMPLES.store(num_samples, Ordering::SeqCst);
155 }
156
157 extern "C" fn mock_set_sample_rate(_instance: *mut c_void, _sample_rate: f32) {
158 SET_SAMPLE_RATE_CALLED.store(true, Ordering::SeqCst);
159 }
160
161 extern "C" fn mock_reset(_instance: *mut c_void) {
162 RESET_CALLED.store(true, Ordering::SeqCst);
163 }
164
165 extern "C" fn mock_drop(_instance: *mut c_void) {
166 DROP_CALLED.store(true, Ordering::SeqCst);
167 }
168
169 fn mock_vtable() -> DevProcessorVTable {
170 DevProcessorVTable {
171 version: wavecraft_protocol::DEV_PROCESSOR_VTABLE_VERSION,
172 create: mock_create,
173 process: mock_process,
174 set_sample_rate: mock_set_sample_rate,
175 reset: mock_reset,
176 drop: mock_drop,
177 }
178 }
179
180 #[test]
181 fn test_ffi_processor_lifecycle() {
182 let _guard = TEST_LOCK.lock().unwrap();
183 reset_flags();
184 let vtable = mock_vtable();
185
186 let mut processor = FfiProcessor::new(&vtable).expect("create should succeed");
187 assert!(CREATE_CALLED.load(Ordering::SeqCst));
188
189 let mut left = vec![0.0f32; 128];
191 let mut right = vec![0.0f32; 128];
192 let mut channels: Vec<&mut [f32]> = vec![&mut left, &mut right];
193 processor.process(&mut channels);
194 assert!(PROCESS_CALLED.load(Ordering::SeqCst));
195 assert_eq!(PROCESS_CHANNELS.load(Ordering::SeqCst), 2);
196 assert_eq!(PROCESS_SAMPLES.load(Ordering::SeqCst), 128);
197
198 drop(processor);
200 assert!(DROP_CALLED.load(Ordering::SeqCst));
201 }
202
203 #[test]
204 fn test_ffi_processor_set_sample_rate_and_reset() {
205 let _guard = TEST_LOCK.lock().unwrap();
206 reset_flags();
207 let vtable = mock_vtable();
208
209 let mut processor = FfiProcessor::new(&vtable).expect("create should succeed");
210
211 processor.set_sample_rate(48000.0);
212 assert!(SET_SAMPLE_RATE_CALLED.load(Ordering::SeqCst));
213
214 processor.reset();
215 assert!(RESET_CALLED.load(Ordering::SeqCst));
216
217 drop(processor);
218 }
219
220 #[test]
221 fn test_ffi_processor_null_create_returns_none() {
222 let _guard = TEST_LOCK.lock().unwrap();
223 reset_flags();
224 let mut vtable = mock_vtable();
225 vtable.create = mock_create_null;
226
227 let result = FfiProcessor::new(&vtable);
228 assert!(CREATE_CALLED.load(Ordering::SeqCst));
229 assert!(
230 result.is_none(),
231 "Should return None when create returns null"
232 );
233 }
234
235 #[test]
236 fn test_ffi_processor_empty_channels_noop() {
237 let _guard = TEST_LOCK.lock().unwrap();
238 reset_flags();
239 let vtable = mock_vtable();
240 let mut processor = FfiProcessor::new(&vtable).expect("create should succeed");
241
242 PROCESS_CALLED.store(false, Ordering::SeqCst);
244 let mut channels: Vec<&mut [f32]> = vec![];
245 processor.process(&mut channels);
246 assert!(
247 !PROCESS_CALLED.load(Ordering::SeqCst),
248 "Should not call vtable.process with empty channels"
249 );
250
251 drop(processor);
252 }
253}