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}