Skip to main content

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}