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