aic_sdk/processor.rs
1use crate::{error::*, model::Model};
2
3use aic_sdk_sys::{AicProcessorParameter::*, *};
4
5use std::{ffi::CString, marker::PhantomData, ptr, sync::Once};
6
7static SET_WRAPPER_ID: Once = Once::new();
8
9/// Audio processing configuration passed to [`Processor::initialize`].
10///
11/// Use [`ProcessorConfig::optimal`] as a starting point, then adjust fields
12/// to match your stream layout.
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub struct ProcessorConfig {
15 /// Sample rate in Hz (8000 - 192000).
16 pub sample_rate: u32,
17 /// Number of audio channels in the stream (1 for mono, 2 for stereo, etc).
18 pub num_channels: u16,
19 /// Samples per channel provided to each processing call.
20 /// Note that using a non-optimal number of frames increases latency.
21 pub num_frames: usize,
22 /// Allows frame counts below `num_frames` at the cost of added latency.
23 pub allow_variable_frames: bool,
24}
25
26impl ProcessorConfig {
27 /// Returns a [`ProcessorConfig`] pre-filled with the model's optimal sample rate and frame size.
28 ///
29 /// `num_channels` will be set to `1` and `allow_variable_frames` to `false`.
30 /// Adjust the number of channels and enable variable frames by using the builder pattern.
31 ///
32 /// ```rust,no_run
33 /// # use aic_sdk::{Model, ProcessorConfig, Processor};
34 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
35 /// # let model = Model::from_file("/path/to/model.aicmodel").unwrap();
36 /// # let processor = Processor::new(&model, &license_key).unwrap();
37 /// let config = ProcessorConfig::optimal(&model)
38 /// .with_num_channels(2)
39 /// .with_allow_variable_frames(true);
40 /// ```
41 ///
42 /// If you need to configure a non-optimal sample rate or number of frames,
43 /// construct the [`ProcessorConfig`] struct directly. For example:
44 /// ```rust,no_run
45 /// # use aic_sdk::{Model, ProcessorConfig};
46 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
47 /// # let model = Model::from_file("/path/to/model.aicmodel").unwrap();
48 /// let config = ProcessorConfig {
49 /// num_channels: 2,
50 /// sample_rate: 44100,
51 /// num_frames: model.optimal_num_frames(44100),
52 /// allow_variable_frames: true,
53 /// };
54 /// ```
55 pub fn optimal(model: &Model) -> Self {
56 let sample_rate = model.optimal_sample_rate();
57 let num_frames = model.optimal_num_frames(sample_rate);
58 ProcessorConfig {
59 sample_rate,
60 num_channels: 1,
61 num_frames,
62 allow_variable_frames: false,
63 }
64 }
65
66 /// Sets the number of audio channels for processing.
67 ///
68 /// # Arguments
69 ///
70 /// * `num_channels` - Number of audio channels (1 for mono, 2 for stereo, etc.)
71 pub fn with_num_channels(mut self, num_channels: u16) -> Self {
72 self.num_channels = num_channels;
73 self
74 }
75
76 /// Enables or disables variable frame size support.
77 ///
78 /// When enabled, allows processing frame counts below `num_frames` at the cost of added latency.
79 ///
80 /// # Arguments
81 ///
82 /// * `allow_variable_frames` - `true` to enable variable frame sizes, `false` for fixed size
83 pub fn with_allow_variable_frames(mut self, allow_variable_frames: bool) -> Self {
84 self.allow_variable_frames = allow_variable_frames;
85 self
86 }
87}
88
89/// Configurable parameters for audio enhancement
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91pub enum ProcessorParameter {
92 /// Controls whether audio processing is bypassed while preserving algorithmic delay.
93 ///
94 /// When enabled, the input audio passes through unmodified, but the output is still
95 /// delayed by the same amount as during normal processing. This ensures seamless
96 /// transitions when toggling enhancement on/off without audible clicks or timing shifts.
97 ///
98 /// **Range:** 0.0 to 1.0
99 /// - **0.0:** Enhancement active (normal processing)
100 /// - **1.0:** Bypass enabled (latency-compensated passthrough)
101 ///
102 /// **Default:** 0.0
103 Bypass,
104 /// Controls the intensity of speech enhancement processing.
105 ///
106 /// **Range:** 0.0 to 1.0
107 /// - **0.0:** Bypass mode - original signal passes through unchanged
108 /// - **1.0:** Full enhancement - maximum noise reduction but also more audible artifacts
109 ///
110 /// **Default:** 1.0
111 EnhancementLevel,
112 /// Compensates for perceived volume reduction after noise removal.
113 ///
114 /// **Range:** 0.1 to 4.0 (linear amplitude multiplier)
115 /// - **0.1:** Significant volume reduction (-20 dB)
116 /// - **1.0:** No gain change (0 dB, default)
117 /// - **2.0:** Double amplitude (+6 dB)
118 /// - **4.0:** Maximum boost (+12 dB)
119 ///
120 /// **Formula:** Gain (dB) = 20 × log₁₀(value)
121 /// **Default:** 1.0
122 VoiceGain,
123}
124
125impl From<ProcessorParameter> for AicProcessorParameter::Type {
126 fn from(parameter: ProcessorParameter) -> Self {
127 match parameter {
128 ProcessorParameter::Bypass => AIC_PROCESSOR_PARAMETER_BYPASS,
129 ProcessorParameter::EnhancementLevel => AIC_PROCESSOR_PARAMETER_ENHANCEMENT_LEVEL,
130 ProcessorParameter::VoiceGain => AIC_PROCESSOR_PARAMETER_VOICE_GAIN,
131 }
132 }
133}
134
135pub struct ProcessorContext {
136 /// Raw pointer to the C processor context structure
137 inner: *mut AicProcessorContext,
138}
139
140impl ProcessorContext {
141 /// Creates a new Processor context.
142 pub(crate) fn new(ctx_ptr: *mut AicProcessorContext) -> Self {
143 Self { inner: ctx_ptr }
144 }
145
146 fn as_const_ptr(&self) -> *const AicProcessorContext {
147 self.inner as *const AicProcessorContext
148 }
149
150 /// Modifies a processor parameter.
151 ///
152 /// All parameters can be changed during audio processing.
153 /// This function can be called from any thread.
154 ///
155 /// # Arguments
156 ///
157 /// * `parameter` - Parameter to modify
158 /// * `value` - New parameter value. See parameter documentation for ranges
159 ///
160 /// # Returns
161 ///
162 /// Returns `Ok(())` on success or an `AicError` if the parameter cannot be set.
163 ///
164 /// # Example
165 ///
166 /// ```rust,no_run
167 /// # use aic_sdk::{Model, ProcessorParameter, Processor};
168 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
169 /// # let model = Model::from_file("/path/to/model.aicmodel").unwrap();
170 /// # let processor = Processor::new(&model, &license_key).unwrap();
171 /// # let proc_ctx = processor.processor_context();
172 /// proc_ctx.set_parameter(ProcessorParameter::EnhancementLevel, 0.8).unwrap();
173 /// ```
174 pub fn set_parameter(&self, parameter: ProcessorParameter, value: f32) -> Result<(), AicError> {
175 // SAFETY:
176 // - `self.as_const_ptr()` is a valid pointer to a live processor context.
177 let error_code = unsafe {
178 aic_processor_context_set_parameter(self.as_const_ptr(), parameter.into(), value)
179 };
180 handle_error(error_code)
181 }
182
183 /// Retrieves the current value of a parameter.
184 ///
185 /// This function can be called from any thread.
186 ///
187 /// # Arguments
188 ///
189 /// * `parameter` - Parameter to query
190 ///
191 /// # Returns
192 ///
193 /// Returns `Ok(value)` containing the current parameter value, or an `AicError` if the query fails.
194 ///
195 /// # Example
196 ///
197 /// ```rust,no_run
198 /// # use aic_sdk::{Model, ProcessorParameter, Processor};
199 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
200 /// # let model = Model::from_file("/path/to/model.aicmodel").unwrap();
201 /// # let processor = Processor::new(&model, &license_key).unwrap();
202 /// # let processor_context = processor.processor_context();
203 /// let enhancement_level = processor_context.parameter(ProcessorParameter::EnhancementLevel).unwrap();
204 /// println!("Current enhancement level: {enhancement_level}");
205 /// ```
206 pub fn parameter(&self, parameter: ProcessorParameter) -> Result<f32, AicError> {
207 let mut value: f32 = 0.0;
208 // SAFETY:
209 // - `self.as_const_ptr()` is a valid pointer to a live processor context.
210 // - `value` points to stack storage for output.
211 let error_code = unsafe {
212 aic_processor_context_get_parameter(self.as_const_ptr(), parameter.into(), &mut value)
213 };
214 handle_error(error_code)?;
215 Ok(value)
216 }
217
218 /// Returns the total output delay in samples for the current audio configuration.
219 ///
220 /// This function provides the complete end-to-end latency introduced by the processor,
221 /// which includes both algorithmic processing delay and any buffering overhead.
222 /// Use this value to synchronize enhanced audio with other streams or to implement
223 /// delay compensation in your application.
224 ///
225 /// **Delay behavior:**
226 /// - **Before initialization:** Returns the base processing delay using the model's
227 /// optimal frame size at its native sample rate
228 /// - **After initialization:** Returns the actual delay for your specific configuration,
229 /// including any additional buffering introduced by non-optimal frame sizes
230 ///
231 /// **Important:** The delay value is always expressed in samples at the sample rate
232 /// you configured during `initialize`. To convert to time units:
233 /// `delay_ms = (delay_samples * 1000) / sample_rate`
234 ///
235 /// **Note:** Using frame sizes different from the optimal value returned by
236 /// `optimal_num_frames` will increase the delay beyond the model's base latency.
237 ///
238 /// # Returns
239 ///
240 /// Returns the delay in samples.
241 ///
242 /// # Example
243 ///
244 /// ```rust,no_run
245 /// # use aic_sdk::{Model, Processor};
246 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
247 /// # let model = Model::from_file("/path/to/model.aicmodel").unwrap();
248 /// # let processor = Processor::new(&model, &license_key).unwrap();
249 /// # let processor_context = processor.processor_context();
250 /// let delay = processor_context.output_delay();
251 /// println!("Output delay: {} samples", delay);
252 /// ```
253 pub fn output_delay(&self) -> usize {
254 let mut delay: usize = 0;
255 // SAFETY:
256 // - `self.as_const_ptr()` is a valid pointer to a live processor context.
257 // - `delay` points to stack storage for output.
258 let error_code =
259 unsafe { aic_processor_context_get_output_delay(self.as_const_ptr(), &mut delay) };
260
261 // This should never fail. If it does, it's a bug in the SDK.
262 // `aic_get_output_delay` is documented to always succeed if given a valid processor pointer.
263 assert_success(
264 error_code,
265 "`aic_get_output_delay` failed. This is a bug, please open an issue on GitHub for further investigation.",
266 );
267
268 delay
269 }
270
271 /// Clears all internal state and buffers.
272 ///
273 /// Call this when the audio stream is interrupted or when seeking
274 /// to prevent artifacts from previous audio content.
275 ///
276 /// The processor stays initialized to the configured settings.
277 ///
278 /// # Returns
279 ///
280 /// Returns `Ok(())` on success or an `AicError` if the reset fails.
281 ///
282 /// # Thread Safety
283 /// Real-time safe. Can be called from audio processing threads.
284 ///
285 /// # Example
286 ///
287 /// ```rust,no_run
288 /// # use aic_sdk::{Model, Processor};
289 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
290 /// # let model = Model::from_file("/path/to/model.aicmodel").unwrap();
291 /// # let processor = Processor::new(&model, &license_key).unwrap();
292 /// # let processor_context = processor.processor_context();
293 /// processor_context.reset().unwrap();
294 /// ```
295 pub fn reset(&self) -> Result<(), AicError> {
296 // SAFETY:
297 // - `self.as_const_ptr()` is a valid pointer to a live processor context.
298 let error_code = unsafe { aic_processor_context_reset(self.as_const_ptr()) };
299 handle_error(error_code)
300 }
301}
302
303impl Drop for ProcessorContext {
304 fn drop(&mut self) {
305 if !self.inner.is_null() {
306 // SAFETY:
307 // - `self.inner` was allocated by the SDK and is still owned by this wrapper.
308 unsafe { aic_processor_context_destroy(self.inner) };
309 }
310 }
311}
312
313// Safety: The underlying C library should be thread-safe for individual ProcessorContext instances
314unsafe impl Send for ProcessorContext {}
315unsafe impl Sync for ProcessorContext {}
316
317/// High-level wrapper for the ai-coustics audio enhancement processor.
318///
319/// This struct provides a safe, Rust-friendly interface to the underlying C library.
320/// It handles memory management automatically and converts C-style error codes
321/// to Rust `Result` types.
322///
323/// # Example
324///
325/// ```rust,no_run
326/// use aic_sdk::{Model, ProcessorConfig, Processor};
327///
328/// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
329/// let model = Model::from_file("/path/to/model.aicmodel").unwrap();
330/// let config = ProcessorConfig {
331/// num_channels: 2,
332/// num_frames: 1024,
333/// ..ProcessorConfig::optimal(&model)
334/// };
335///
336/// let mut processor = Processor::new(&model, &license_key).unwrap();
337/// processor.initialize(&config).unwrap();
338///
339/// let mut audio_buffer = vec![0.0f32; config.num_channels as usize * config.num_frames];
340/// processor.process_interleaved(&mut audio_buffer).unwrap();
341/// ```
342pub struct Processor<'a> {
343 /// Raw pointer to the C processor structure
344 inner: *mut AicProcessor,
345 /// Configured number of channels
346 num_channels: Option<u16>,
347 /// Marker to tie the lifetime of the processor to the lifetime of the model's weights
348 marker: PhantomData<&'a [u8]>,
349}
350
351impl<'a> Processor<'a> {
352 /// Creates a new audio enhancement processor instance.
353 ///
354 /// Multiple processors can be created to process different audio streams simultaneously
355 /// or to switch between different enhancement algorithms during runtime.
356 ///
357 /// # Arguments
358 ///
359 /// * `model` - The loaded model instance
360 /// * `license_key` - license key for the ai-coustics SDK
361 /// (generate your key at [developers.ai-coustics.com](https://developers.ai-coustics.com/))
362 ///
363 /// # Returns
364 ///
365 /// Returns a `Result` containing the new `Processor` instance or an `AicError` if creation fails.
366 ///
367 /// # Example
368 ///
369 /// ```rust,no_run
370 /// # use aic_sdk::{Model, Processor};
371 /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
372 /// let model = Model::from_file("/path/to/model.aicmodel").unwrap();
373 /// let processor = Processor::new(&model, &license_key).unwrap();
374 /// ```
375 pub fn new(model: &Model<'a>, license_key: &str) -> Result<Self, AicError> {
376 SET_WRAPPER_ID.call_once(|| unsafe {
377 // SAFETY:
378 // - This FFI call has no safety requirements.
379 aic_set_sdk_wrapper_id(2);
380 });
381
382 let mut processor_ptr: *mut AicProcessor = ptr::null_mut();
383 let c_license_key =
384 CString::new(license_key).map_err(|_| AicError::LicenseFormatInvalid)?;
385
386 // SAFETY:
387 // - `processor_ptr` points to stack storage for output.
388 // - `model` is a valid SDK model pointer for the duration of the call.
389 // - `c_license_key` is a null-terminated CString.
390 let error_code = unsafe {
391 aic_processor_create(
392 &mut processor_ptr,
393 model.as_const_ptr(),
394 c_license_key.as_ptr(),
395 )
396 };
397
398 handle_error(error_code)?;
399
400 // This should never happen if the C library is well-behaved, but let's be defensive
401 assert!(
402 !processor_ptr.is_null(),
403 "C library returned success but null pointer"
404 );
405
406 Ok(Self {
407 inner: processor_ptr,
408 num_channels: None,
409 marker: PhantomData,
410 })
411 }
412
413 /// Initializes the processor with the given configuration.
414 ///
415 /// This is a convenience method that calls [`Processor::initialize`] internally and returns `self`.
416 /// The processor is immediately ready to process audio after calling this method, so you don't
417 /// need to call [`Processor::initialize`] separately.
418 ///
419 /// # Arguments
420 ///
421 /// * `config` - Audio processing configuration
422 ///
423 /// # Returns
424 ///
425 /// Returns `Ok(Self)` with the initialized processor, or an `AicError` if initialization fails.
426 ///
427 /// # Example
428 ///
429 /// ```rust,no_run
430 /// # use aic_sdk::{Model, Processor, ProcessorConfig};
431 /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
432 /// let model = Model::from_file("/path/to/model.aicmodel")?;
433 /// let config = ProcessorConfig::optimal(&model).with_num_channels(2);
434 ///
435 /// let mut processor = Processor::new(&model, &license_key)?.with_config(&config)?;
436 ///
437 /// // Processor is ready to use - no need to call initialize()
438 /// let mut audio = vec![0.0f32; config.num_channels as usize * config.num_frames];
439 /// processor.process_interleaved(&mut audio).unwrap();
440 /// # Ok::<(), aic_sdk::AicError>(())
441 /// ```
442 pub fn with_config(mut self, config: &ProcessorConfig) -> Result<Self, AicError> {
443 self.initialize(config)?;
444 Ok(self)
445 }
446
447 /// Creates a [ProcessorContext] instance.
448 /// This can be used to control all parameters and other settings of the processor.
449 ///
450 /// # Example
451 ///
452 /// ```rust,no_run
453 /// # use aic_sdk::{Model, Processor};
454 /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
455 /// let model = Model::from_file("/path/to/model.aicmodel").unwrap();
456 /// let processor = Processor::new(&model, &license_key).unwrap();
457 /// let processor_context = processor.processor_context();
458 /// ```
459 pub fn processor_context(&self) -> ProcessorContext {
460 let mut processor_context: *mut AicProcessorContext = ptr::null_mut();
461
462 // SAFETY:
463 // - `processor_context` is valid output storage.
464 // - `self.as_const_ptr()` is a live processor pointer.
465 let error_code =
466 unsafe { aic_processor_context_create(&mut processor_context, self.as_const_ptr()) };
467
468 // This should never fail
469 assert!(handle_error(error_code).is_ok());
470
471 // This should never happen if the C library is well-behaved, but let's be defensive
472 assert!(
473 !processor_context.is_null(),
474 "C library returned success but null pointer"
475 );
476
477 ProcessorContext::new(processor_context)
478 }
479
480 /// Creates a [Voice Activity Detector Context](crate::vad::VadContext) instance.
481 ///
482 /// # Example
483 ///
484 /// ```rust,no_run
485 /// # use aic_sdk::{Model, Processor};
486 /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
487 /// let model = Model::from_file("/path/to/model.aicmodel").unwrap();
488 /// let processor = Processor::new(&model, &license_key).unwrap();
489 /// let vad = processor.vad_context();
490 /// ```
491 pub fn vad_context(&self) -> crate::VadContext {
492 let mut vad_ptr: *mut AicVadContext = ptr::null_mut();
493
494 // SAFETY:
495 // - `vad_ptr` is valid output storage.
496 // - `self.as_const_ptr()` is a live processor pointer.
497 let error_code = unsafe { aic_vad_context_create(&mut vad_ptr, self.as_const_ptr()) };
498
499 // This should never fail
500 assert!(handle_error(error_code).is_ok());
501
502 // This should never happen if the C library is well-behaved, but let's be defensive
503 assert!(
504 !vad_ptr.is_null(),
505 "C library returned success but null pointer"
506 );
507
508 crate::vad::VadContext::new(vad_ptr)
509 }
510
511 /// Configures the processor for specific audio settings.
512 ///
513 /// This function must be called before processing any audio.
514 /// For the lowest delay use the sample rate and frame size returned by
515 /// [`Model::optimal_sample_rate`] and [`Model::optimal_num_frames`].
516 ///
517 /// # Arguments
518 ///
519 /// * `config` - Audio processing configuration
520 ///
521 /// # Returns
522 ///
523 /// Returns `Ok(())` on success or an `AicError` if initialization fails.
524 ///
525 /// # Warning
526 /// Do not call from audio processing threads as this allocates memory.
527 ///
528 /// # Note
529 /// All channels are mixed to mono for processing. To process channels
530 /// independently, create separate [`Processor`] instances.
531 ///
532 /// # Example
533 ///
534 /// ```rust,no_run
535 /// # use aic_sdk::{Model, Processor, ProcessorConfig};
536 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
537 /// # let model = Model::from_file("/path/to/model.aicmodel").unwrap();
538 /// # let mut processor = Processor::new(&model, &license_key).unwrap();
539 /// let config = ProcessorConfig::optimal(&model);
540 /// processor.initialize(&config).unwrap();
541 /// ```
542 pub fn initialize(&mut self, config: &ProcessorConfig) -> Result<(), AicError> {
543 // SAFETY:
544 // - `self.inner` is a valid pointer to a live processor.
545 let error_code = unsafe {
546 aic_processor_initialize(
547 self.inner,
548 config.sample_rate,
549 config.num_channels,
550 config.num_frames,
551 config.allow_variable_frames,
552 )
553 };
554
555 handle_error(error_code)?;
556 self.num_channels = Some(config.num_channels);
557 Ok(())
558 }
559
560 /// Processes audio with separate buffers for each channel (planar layout).
561 ///
562 /// Enhances speech in the provided audio buffers in-place.
563 ///
564 /// **Memory Layout:**
565 /// - Separate buffer for each channel
566 /// - Each buffer contains `num_frames` floats
567 /// - Maximum of 16 channels supported
568 /// - Example for 2 channels, 4 frames:
569 /// ```text
570 /// audio[0] -> [ch0_f0, ch0_f1, ch0_f2, ch0_f3]
571 /// audio[1] -> [ch1_f0, ch1_f1, ch1_f2, ch1_f3]
572 /// ```
573 ///
574 /// The function accepts any type of collection of `f32` values that implements `as_mut`, e.g.:
575 /// - `[vec![0.0; 128]; 2]`
576 /// - `[[0.0; 128]; 2]`
577 /// - `[&mut ch1, &mut ch2]`
578 ///
579 /// # Arguments
580 ///
581 /// * `audio` - Array of mutable channel buffer slices to be enhanced in-place.
582 /// Each channel buffer must be exactly of size `num_frames`,
583 /// or if `allow_variable_frames` was enabled, less than the initialization value.
584 ///
585 /// # Note
586 ///
587 /// Maximum supported number of channels is 16. Exceeding this will return an error.
588 ///
589 /// # Returns
590 ///
591 /// Returns `Ok(())` on success or an `AicError` if processing fails.
592 ///
593 /// # Example
594 ///
595 /// ```rust,no_run
596 /// # use aic_sdk::{Model, Processor, ProcessorConfig};
597 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
598 /// # let model = Model::from_file("/path/to/model.aicmodel").unwrap();
599 /// # let mut processor = Processor::new(&model, &license_key).unwrap();
600 /// let config = ProcessorConfig::optimal(&model).with_num_channels(2);
601 /// processor.initialize(&config).unwrap();
602 /// let mut audio = vec![vec![0.0f32; config.num_frames]; config.num_channels as usize];
603 /// processor.process_planar(&mut audio).unwrap();
604 /// ```
605 #[allow(clippy::doc_overindented_list_items)]
606 pub fn process_planar<V: AsMut<[f32]>>(&mut self, audio: &mut [V]) -> Result<(), AicError> {
607 const MAX_CHANNELS: u16 = 16;
608
609 let Some(num_channels) = self.num_channels else {
610 return Err(AicError::ProcessorNotInitialized);
611 };
612
613 if audio.len() != num_channels as usize {
614 return Err(AicError::AudioConfigMismatch);
615 }
616
617 if num_channels > MAX_CHANNELS {
618 return Err(AicError::AudioConfigUnsupported);
619 }
620
621 let num_frames = if audio.is_empty() {
622 0
623 } else {
624 audio[0].as_mut().len()
625 };
626
627 let mut audio_ptrs = [std::ptr::null_mut::<f32>(); MAX_CHANNELS as usize];
628 for (i, channel) in audio.iter_mut().enumerate() {
629 // Check that all channels have the same number of frames
630 if channel.as_mut().len() != num_frames {
631 return Err(AicError::AudioConfigMismatch);
632 }
633 audio_ptrs[i] = channel.as_mut().as_mut_ptr();
634 }
635
636 // SAFETY:
637 // - `self.inner` is a valid pointer to a live processor.
638 // - `audio_ptrs` holds `num_channels` valid, writable pointers with `num_frames` samples each.
639 let error_code = unsafe {
640 aic_processor_process_planar(self.inner, audio_ptrs.as_ptr(), num_channels, num_frames)
641 };
642
643 handle_error(error_code)
644 }
645
646 /// Processes audio with interleaved channel data.
647 ///
648 /// Enhances speech in the provided audio buffer in-place.
649 ///
650 /// **Memory Layout:**
651 /// - Single contiguous buffer with samples alternating between channels
652 /// - Buffer size: `num_channels` * `num_frames` floats
653 /// - Example for 2 channels, 4 frames:
654 /// ```text
655 /// audio -> [ch0_f0, ch1_f0, ch0_f1, ch1_f1, ch0_f2, ch1_f2, ch0_f3, ch1_f3]
656 /// ```
657 ///
658 /// # Arguments
659 ///
660 /// * `audio` - Interleaved audio buffer to be enhanced in-place.
661 /// Must be exactly of size `num_channels` * `num_frames`,
662 /// or if `allow_variable_frames` was enabled, less than the initialization value per channel.
663 ///
664 /// # Returns
665 ///
666 /// Returns `Ok(())` on success or an `AicError` if processing fails.
667 ///
668 /// # Example
669 ///
670 /// ```rust,no_run
671 /// # use aic_sdk::{Model, Processor, ProcessorConfig};
672 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
673 /// # let model = Model::from_file("/path/to/model.aicmodel").unwrap();
674 /// # let mut processor = Processor::new(&model, &license_key).unwrap();
675 /// let config = ProcessorConfig::optimal(&model).with_num_channels(2);
676 /// processor.initialize(&config).unwrap();
677 /// let mut audio = vec![0.0f32; config.num_channels as usize * config.num_frames];
678 /// processor.process_interleaved(&mut audio).unwrap();
679 /// ```
680 #[allow(clippy::doc_overindented_list_items)]
681 pub fn process_interleaved(&mut self, audio: &mut [f32]) -> Result<(), AicError> {
682 let Some(num_channels) = self.num_channels else {
683 return Err(AicError::ProcessorNotInitialized);
684 };
685
686 if !audio.len().is_multiple_of(num_channels as usize) {
687 return Err(AicError::AudioConfigMismatch);
688 }
689
690 let num_frames = audio.len() / num_channels as usize;
691
692 // SAFETY:
693 // - `self.inner` is a valid pointer to a live processor.
694 // - `audio` points to a contiguous f32 slice of length `num_channels * num_frames`.
695 let error_code = unsafe {
696 aic_processor_process_interleaved(
697 self.inner,
698 audio.as_mut_ptr(),
699 num_channels,
700 num_frames,
701 )
702 };
703
704 handle_error(error_code)
705 }
706
707 /// Processes audio with sequential channel data.
708 ///
709 /// Enhances speech in the provided audio buffer in-place.
710 ///
711 /// **Memory Layout:**
712 /// - Single contiguous buffer with all samples for each channel stored sequentially
713 /// - Buffer size: `num_channels` * `num_frames` floats
714 /// - Example for 2 channels, 4 frames:
715 /// ```text
716 /// audio -> [ch0_f0, ch0_f1, ch0_f2, ch0_f3, ch1_f0, ch1_f1, ch1_f2, ch1_f3]
717 /// ```
718 ///
719 /// # Arguments
720 ///
721 /// * `audio` - Sequential audio buffer to be enhanced in-place.
722 /// Must be exactly of size `num_channels` * `num_frames`,
723 /// or if `allow_variable_frames` was enabled, less than the initialization value per channel.
724 ///
725 /// # Returns
726 ///
727 /// Returns `Ok(())` on success or an `AicError` if processing fails.
728 ///
729 /// # Example
730 ///
731 /// ```rust,no_run
732 /// # use aic_sdk::{Model, Processor, ProcessorConfig};
733 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
734 /// # let model = Model::from_file("/path/to/model.aicmodel").unwrap();
735 /// # let mut processor = Processor::new(&model, &license_key).unwrap();
736 /// let config = ProcessorConfig::optimal(&model).with_num_channels(2);;
737 /// processor.initialize(&config).unwrap();
738 /// let mut audio = vec![0.0f32; config.num_channels as usize * config.num_frames];
739 /// processor.process_sequential(&mut audio).unwrap();
740 /// ```
741 #[allow(clippy::doc_overindented_list_items)]
742 pub fn process_sequential(&mut self, audio: &mut [f32]) -> Result<(), AicError> {
743 let Some(num_channels) = self.num_channels else {
744 return Err(AicError::ProcessorNotInitialized);
745 };
746
747 if !audio.len().is_multiple_of(num_channels as usize) {
748 return Err(AicError::AudioConfigMismatch);
749 }
750
751 let num_frames = audio.len() / num_channels as usize;
752
753 // SAFETY:
754 // - `self.inner` is a valid pointer to a live, initialized processor.
755 // - `audio` points to a contiguous f32 slice of length `num_channels * num_frames`.
756 let error_code = unsafe {
757 aic_processor_process_sequential(
758 self.inner,
759 audio.as_mut_ptr(),
760 num_channels,
761 num_frames,
762 )
763 };
764
765 handle_error(error_code)
766 }
767
768 fn as_const_ptr(&self) -> *const AicProcessor {
769 self.inner as *const AicProcessor
770 }
771}
772
773impl<'a> Drop for Processor<'a> {
774 fn drop(&mut self) {
775 if !self.inner.is_null() {
776 // SAFETY:
777 // - `self.inner` was allocated by the SDK and is still owned by this wrapper.
778 unsafe { aic_processor_destroy(self.inner) };
779 }
780 }
781}
782
783// SAFETY: Everything in Processor is Send, with the exception of the inner raw pointer.
784// The Processor only uses the raw pointer according to the safety contracts of the
785// unsafe APIs that require the pointer, and the Processor does not expose access to the
786// raw pointer in any of its methods. Therefore, it safe to implement Send for Processor.
787unsafe impl<'a> Send for Processor<'a> {}
788
789// SAFETY: Processor does not expose any interior mutability, and all unsafe APIs that make use of
790// the inner raw pointer are only used in methods that take &mut self, which upholds the thread safety
791// contracts required by the unsafe APIs. Therefore, it is safe to implement Sync for Processor.
792unsafe impl<'a> Sync for Processor<'a> {}
793
794#[cfg(test)]
795mod tests {
796 use super::*;
797 use std::{
798 fs,
799 path::{Path, PathBuf},
800 sync::{Mutex, OnceLock},
801 };
802
803 fn download_lock() -> &'static Mutex<()> {
804 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
805 LOCK.get_or_init(|| Mutex::new(()))
806 }
807
808 fn find_existing_model(target_dir: &Path) -> Option<PathBuf> {
809 let entries = fs::read_dir(target_dir).ok()?;
810 for entry in entries.flatten() {
811 let path = entry.path();
812 if path
813 .file_name()
814 .and_then(|n| n.to_str())
815 .map(|name| name.contains("sparrow_xxs_48khz") && name.ends_with(".aicmodel"))
816 .unwrap_or(false)
817 {
818 if path.is_file() {
819 return Some(path);
820 }
821 }
822 }
823 None
824 }
825
826 /// Downloads the default test model `sparrow-xxs-48khz` into the crate's `target/` directory.
827 /// Returns the path to the downloaded model file.
828 fn get_sparrow_xxs_48khz() -> Result<PathBuf, AicError> {
829 let target_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target");
830
831 if let Some(existing) = find_existing_model(&target_dir) {
832 return Ok(existing);
833 }
834
835 let _guard = download_lock().lock().unwrap();
836 if let Some(existing) = find_existing_model(&target_dir) {
837 return Ok(existing);
838 }
839
840 #[cfg(feature = "download-model")]
841 {
842 return Model::download("sparrow-xxs-48khz", target_dir);
843 }
844
845 #[cfg(not(feature = "download-model"))]
846 {
847 panic!(
848 "Model `sparrow-xxs-48khz` not found in {} and `download-model` feature is disabled",
849 target_dir.display()
850 );
851 }
852 }
853
854 fn load_test_model() -> Result<(Model<'static>, String), AicError> {
855 let license_key = std::env::var("AIC_SDK_LICENSE")
856 .expect("AIC_SDK_LICENSE environment variable must be set for tests");
857
858 let model_path = get_sparrow_xxs_48khz()?;
859 let model = Model::from_file(&model_path)?;
860
861 Ok((model, license_key))
862 }
863
864 #[test]
865 fn model_creation_and_basic_operations() {
866 dbg!(crate::get_sdk_version());
867 dbg!(crate::get_compatible_model_version());
868
869 let (model, license_key) = load_test_model().unwrap();
870 let config = ProcessorConfig::optimal(&model).with_num_channels(2);
871
872 let mut processor = Processor::new(&model, &license_key)
873 .unwrap()
874 .with_config(&config)
875 .unwrap();
876
877 let num_channels = config.num_channels as usize;
878 let mut audio = vec![vec![0.0f32; config.num_frames]; num_channels];
879 let mut audio_refs: Vec<&mut [f32]> =
880 audio.iter_mut().map(|ch| ch.as_mut_slice()).collect();
881
882 processor.process_planar(&mut audio_refs).unwrap();
883 }
884
885 #[test]
886 fn process_interleaved_fixed_frames() {
887 let (model, license_key) = load_test_model().unwrap();
888 let config = ProcessorConfig::optimal(&model).with_num_channels(2);
889
890 let mut processor = Processor::new(&model, &license_key)
891 .unwrap()
892 .with_config(&config)
893 .unwrap();
894
895 let num_channels = config.num_channels as usize;
896 let mut audio = vec![0.0f32; num_channels * config.num_frames];
897 processor.process_interleaved(&mut audio).unwrap();
898 }
899
900 #[test]
901 fn process_planar_fixed_frames() {
902 let (model, license_key) = load_test_model().unwrap();
903 let config = ProcessorConfig::optimal(&model).with_num_channels(2);
904
905 let mut processor = Processor::new(&model, &license_key)
906 .unwrap()
907 .with_config(&config)
908 .unwrap();
909
910 let mut left = vec![0.0f32; config.num_frames];
911 let mut right = vec![0.0f32; config.num_frames];
912 let mut audio = [left.as_mut_slice(), right.as_mut_slice()];
913 processor.process_planar(&mut audio).unwrap();
914 }
915
916 #[test]
917 fn process_sequential_fixed_frames() {
918 let (model, license_key) = load_test_model().unwrap();
919 let config = ProcessorConfig::optimal(&model).with_num_channels(2);
920
921 let mut processor = Processor::new(&model, &license_key)
922 .unwrap()
923 .with_config(&config)
924 .unwrap();
925
926 let num_channels = config.num_channels as usize;
927 let mut audio = vec![0.0f32; num_channels * config.num_frames];
928 processor.process_sequential(&mut audio).unwrap();
929 }
930
931 #[test]
932 fn process_interleaved_variable_frames() {
933 let (model, license_key) = load_test_model().unwrap();
934 let config = ProcessorConfig::optimal(&model)
935 .with_num_channels(2)
936 .with_allow_variable_frames(true);
937
938 let mut processor = Processor::new(&model, &license_key)
939 .unwrap()
940 .with_config(&config)
941 .unwrap();
942
943 let num_channels = config.num_channels as usize;
944 let mut audio = vec![0.0f32; num_channels * config.num_frames];
945 processor.process_interleaved(&mut audio).unwrap();
946
947 let mut audio = vec![0.0f32; num_channels * 20];
948 processor.process_interleaved(&mut audio).unwrap();
949 }
950
951 #[test]
952 fn process_planar_variable_frames() {
953 let (model, license_key) = load_test_model().unwrap();
954 let config = ProcessorConfig::optimal(&model)
955 .with_num_channels(2)
956 .with_allow_variable_frames(true);
957
958 let mut processor = Processor::new(&model, &license_key)
959 .unwrap()
960 .with_config(&config)
961 .unwrap();
962
963 let mut left = vec![0.0f32; config.num_frames];
964 let mut right = vec![0.0f32; config.num_frames];
965 let mut audio = [left.as_mut_slice(), right.as_mut_slice()];
966 processor.process_planar(&mut audio).unwrap();
967
968 let mut left = vec![0.0f32; 20];
969 let mut right = vec![0.0f32; 20];
970 let mut audio = [left.as_mut_slice(), right.as_mut_slice()];
971 processor.process_planar(&mut audio).unwrap();
972 }
973
974 #[test]
975 fn process_sequential_variable_frames() {
976 let (model, license_key) = load_test_model().unwrap();
977 let config = ProcessorConfig::optimal(&model)
978 .with_num_channels(2)
979 .with_allow_variable_frames(true);
980
981 let mut processor = Processor::new(&model, &license_key)
982 .unwrap()
983 .with_config(&config)
984 .unwrap();
985
986 let num_channels = config.num_channels as usize;
987 let mut audio = vec![0.0f32; num_channels * config.num_frames];
988 processor.process_sequential(&mut audio).unwrap();
989
990 let mut audio = vec![0.0f32; num_channels * 20];
991 processor.process_sequential(&mut audio).unwrap();
992 }
993
994 #[test]
995 fn process_interleaved_variable_frames_fails_without_allow_variable_frames() {
996 let (model, license_key) = load_test_model().unwrap();
997 let config = ProcessorConfig::optimal(&model).with_num_channels(2);
998
999 let mut processor = Processor::new(&model, &license_key)
1000 .unwrap()
1001 .with_config(&config)
1002 .unwrap();
1003
1004 let num_channels = config.num_channels as usize;
1005 let mut audio = vec![0.0f32; num_channels * config.num_frames];
1006 processor.process_interleaved(&mut audio).unwrap();
1007
1008 let mut audio = vec![0.0f32; num_channels * 20];
1009 let result = processor.process_interleaved(&mut audio);
1010 assert_eq!(result, Err(AicError::AudioConfigMismatch));
1011 }
1012
1013 #[test]
1014 fn process_planar_variable_frames_fails_without_allow_variable_frames() {
1015 let (model, license_key) = load_test_model().unwrap();
1016 let config = ProcessorConfig::optimal(&model).with_num_channels(2);
1017
1018 let mut processor = Processor::new(&model, &license_key)
1019 .unwrap()
1020 .with_config(&config)
1021 .unwrap();
1022
1023 let mut left = vec![0.0f32; config.num_frames];
1024 let mut right = vec![0.0f32; config.num_frames];
1025 let mut audio = [left.as_mut_slice(), right.as_mut_slice()];
1026 processor.process_planar(&mut audio).unwrap();
1027
1028 let mut left = vec![0.0f32; 20];
1029 let mut right = vec![0.0f32; 20];
1030 let mut audio = [left.as_mut_slice(), right.as_mut_slice()];
1031 let result = processor.process_planar(&mut audio);
1032 assert_eq!(result, Err(AicError::AudioConfigMismatch));
1033 }
1034
1035 #[test]
1036 fn process_sequential_variable_frames_fails_without_allow_variable_frames() {
1037 let (model, license_key) = load_test_model().unwrap();
1038 let config = ProcessorConfig::optimal(&model).with_num_channels(2);
1039
1040 let mut processor = Processor::new(&model, &license_key)
1041 .unwrap()
1042 .with_config(&config)
1043 .unwrap();
1044
1045 let num_channels = config.num_channels as usize;
1046 let mut audio = vec![0.0f32; num_channels * config.num_frames];
1047 processor.process_sequential(&mut audio).unwrap();
1048
1049 let mut audio = vec![0.0f32; num_channels * 20];
1050 let result = processor.process_sequential(&mut audio);
1051 assert_eq!(result, Err(AicError::AudioConfigMismatch));
1052 }
1053
1054 #[test]
1055 fn model_can_be_dropped_after_creating_processor() {
1056 let (model, license_key) = load_test_model().unwrap();
1057 let config = ProcessorConfig::optimal(&model).with_num_channels(2);
1058
1059 let mut processor = Processor::new(&model, &license_key)
1060 .unwrap()
1061 .with_config(&config)
1062 .unwrap();
1063 drop(model); // Inside of the SDK an Arc-Pointer to `Model` is stored in Processor, so it won't be de-allocated
1064
1065 let num_channels = config.num_channels as usize;
1066 let mut audio = vec![vec![0.0f32; config.num_frames]; num_channels];
1067 let mut audio_refs: Vec<&mut [f32]> =
1068 audio.iter_mut().map(|ch| ch.as_mut_slice()).collect();
1069
1070 processor.process_planar(&mut audio_refs).unwrap();
1071 }
1072
1073 #[test]
1074 fn processor_is_send_and_sync() {
1075 // Compile-time check that Processor implements Send and Sync.
1076 // This ensures the processor can be safely moved to another thread.
1077 fn assert_send<T: Send>() {}
1078 fn assert_sync<T: Send>() {}
1079
1080 assert_send::<Processor>();
1081 assert_sync::<Processor>();
1082 }
1083
1084 struct MyModel {
1085 _model: Model<'static>,
1086 _processor: Processor<'static>,
1087 }
1088
1089 impl MyModel {
1090 pub fn new() -> Self {
1091 let (model, license_key) = load_test_model().unwrap();
1092 let processor = Processor::new(&model, &license_key)
1093 .unwrap()
1094 .with_config(&ProcessorConfig::optimal(&model))
1095 .unwrap();
1096 MyModel {
1097 _model: model,
1098 _processor: processor,
1099 }
1100 }
1101 }
1102
1103 #[test]
1104 fn can_create_self_referential_structs_with_statics() {
1105 let _model = MyModel::new();
1106 }
1107}
1108
1109#[doc(hidden)]
1110mod _compile_fail_tests {
1111 //! Compile-fail regression: a `Processor`'s model buffer must not be dropped before the processor.
1112 //!
1113 //! ```rust,compile_fail
1114 //! use aic_sdk::{Model, Processor, ProcessorConfig};
1115 //!
1116 //! fn main() {
1117 //! let buffer = vec![0u8; 64];
1118 //! let model = Model::from_buffer(&buffer).unwrap();
1119 //! let config = ProcessorConfig::optimal(&model).with_num_channels(2);
1120 //!
1121 //! let mut processor = Processor::new(&model, "license")
1122 //! .unwrap()
1123 //! .with_config(&config)
1124 //! .unwrap();
1125 //!
1126 //! drop(model); // Model can be dropped without issues
1127 //!
1128 //! drop(buffer); // This should fail to compile
1129 //!
1130 //! let num_channels = config.num_channels as usize;
1131 //! let mut audio = vec![vec![0.0f32; config.num_frames]; num_channels];
1132 //! processor.process_planar(&mut audio).unwrap();
1133 //! }
1134 //! ```
1135}