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};
6
7/// Audio processing configuration passed to [`Processor::initialize`].
8///
9/// Use [`ProcessorConfig::optimal`] as a starting point, then adjust fields
10/// to match your stream layout.
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub struct ProcessorConfig {
13    /// Sample rate in Hz (8000 - 192000).
14    pub sample_rate: u32,
15    /// Number of audio channels in the stream (1 for mono, 2 for stereo, etc).
16    pub num_channels: u16,
17    /// Samples per channel provided to each processing call.
18    /// Note that using a non-optimal number of frames increases latency.
19    pub num_frames: usize,
20    /// Allows frame counts below `num_frames` at the cost of added latency.
21    pub allow_variable_frames: bool,
22}
23
24impl ProcessorConfig {
25    /// Returns a [`ProcessorConfig`] pre-filled with the model's optimal sample rate and frame size.
26    ///
27    /// `num_channels` will be set to `1` and `allow_variable_frames` to `false`.
28    /// Adjust the number of channels and enable variable frames by using the builder pattern.
29    ///
30    /// ```rust,no_run
31    /// # use aic_sdk::{Model, ProcessorConfig, Processor};
32    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
33    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
34    /// # let processor = Processor::new(&model, &license_key)?;
35    /// let config = ProcessorConfig::optimal(&model)
36    ///     .with_num_channels(2)
37    ///     .with_allow_variable_frames(true);
38    /// # Ok::<(), aic_sdk::AicError>(())
39    /// ```
40    ///
41    /// If you need to configure a non-optimal sample rate or number of frames,
42    /// construct the [`ProcessorConfig`] struct directly. For example:
43    /// ```rust,no_run
44    /// # use aic_sdk::{Model, ProcessorConfig};
45    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
46    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
47    /// let config = ProcessorConfig {
48    ///     num_channels: 2,
49    ///     sample_rate: 44100,
50    ///     num_frames: model.optimal_num_frames(44100),
51    ///     allow_variable_frames: true,
52    /// };
53    /// # Ok::<(), aic_sdk::AicError>(())
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
126/// OpenTelemetry configuration for a [`Processor`].
127///
128/// Pass to [`Processor::with_otel_config`] to control telemetry on a per-processor
129/// basis. When no [`OtelConfig`] is provided (e.g. when using [`Processor::new`]), telemetry
130/// is configured according to the runtime environment (e.g. the `AIC_SDK_OTEL_ENABLE`
131/// environment variable).
132#[derive(Debug, Clone, PartialEq, Eq, Hash)]
133pub struct OtelConfig {
134    /// Whether to enable OpenTelemetry telemetry.
135    ///
136    /// Overrides the `AIC_SDK_OTEL_ENABLE` environment variable.
137    pub enable: bool,
138    /// Optional session ID for telemetry. If `None`, a random session ID is generated.
139    pub session_id: Option<String>,
140    /// OpenTelemetry metric export interval in milliseconds.
141    ///
142    /// Set to `0` to use the SDK default of 60 000 ms.
143    pub export_interval_ms: u32,
144}
145
146impl OtelConfig {
147    /// Returns an [`OtelConfig`] with telemetry disabled.
148    pub fn disabled() -> Self {
149        Self {
150            enable: false,
151            session_id: None,
152            export_interval_ms: 0,
153        }
154    }
155
156    /// Returns an [`OtelConfig`] with telemetry enabled.
157    pub fn enabled() -> Self {
158        Self {
159            enable: true,
160            session_id: None,
161            export_interval_ms: 0,
162        }
163    }
164
165    /// Returns an [`OtelConfig`] with telemetry enabled and the provided session ID.
166    pub fn with_session_id(session_id: impl Into<String>) -> Self {
167        Self {
168            enable: true,
169            session_id: Some(session_id.into()),
170            export_interval_ms: 0,
171        }
172    }
173}
174
175pub struct ProcessorContext {
176    /// Raw pointer to the C processor context structure
177    inner: *mut AicProcessorContext,
178}
179
180impl ProcessorContext {
181    /// Creates a new Processor context.
182    pub(crate) fn new(ctx_ptr: *mut AicProcessorContext) -> Self {
183        Self { inner: ctx_ptr }
184    }
185
186    fn as_const_ptr(&self) -> *const AicProcessorContext {
187        self.inner as *const AicProcessorContext
188    }
189
190    /// Modifies a processor parameter.
191    ///
192    /// All parameters can be changed during audio processing.
193    /// This function can be called from any thread.
194    ///
195    /// # Arguments
196    ///
197    /// * `parameter` - Parameter to modify
198    /// * `value` - New parameter value. See parameter documentation for ranges
199    ///
200    /// # Returns
201    ///
202    /// Returns `Ok(())` on success or an [`AicError`] if the parameter cannot be set.
203    ///
204    /// # Example
205    ///
206    /// ```rust,no_run
207    /// # use aic_sdk::{Model, ProcessorParameter, Processor};
208    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
209    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
210    /// # let processor = Processor::new(&model, &license_key)?;
211    /// # let proc_ctx = processor.processor_context();
212    /// proc_ctx.set_parameter(ProcessorParameter::EnhancementLevel, 0.8)?;
213    /// # Ok::<(), aic_sdk::AicError>(())
214    /// ```
215    pub fn set_parameter(&self, parameter: ProcessorParameter, value: f32) -> Result<(), AicError> {
216        // SAFETY:
217        // - `self.as_const_ptr()` is a valid pointer to a live processor context.
218        // - This function can be called from any thread, so we only borrow `&self`.
219        let error_code = unsafe {
220            aic_processor_context_set_parameter(self.as_const_ptr(), parameter.into(), value)
221        };
222        handle_error(error_code)
223    }
224
225    /// Retrieves the current value of a parameter.
226    ///
227    /// This function can be called from any thread.
228    ///
229    /// # Arguments
230    ///
231    /// * `parameter` - Parameter to query
232    ///
233    /// # Returns
234    ///
235    /// Returns `Ok(value)` containing the current parameter value, or an [`AicError`] if the query fails.
236    ///
237    /// # Example
238    ///
239    /// ```rust,no_run
240    /// # use aic_sdk::{Model, ProcessorParameter, Processor};
241    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
242    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
243    /// # let processor = Processor::new(&model, &license_key)?;
244    /// # let processor_context = processor.processor_context();
245    /// let enhancement_level = processor_context.parameter(ProcessorParameter::EnhancementLevel)?;
246    /// println!("Current enhancement level: {enhancement_level}");
247    /// # Ok::<(), aic_sdk::AicError>(())
248    /// ```
249    pub fn parameter(&self, parameter: ProcessorParameter) -> Result<f32, AicError> {
250        let mut value: f32 = 0.0;
251        // SAFETY:
252        // - `self.as_const_ptr()` is a valid pointer to a live processor context.
253        // - `value` points to stack storage for output.
254        // - This function can be called from any thread, so we only borrow `&self`.
255        let error_code = unsafe {
256            aic_processor_context_get_parameter(self.as_const_ptr(), parameter.into(), &mut value)
257        };
258        handle_error(error_code)?;
259        Ok(value)
260    }
261
262    /// Returns the total output delay in samples for the current audio configuration.
263    ///
264    /// This function provides the complete end-to-end latency introduced by the processor,
265    /// which includes both algorithmic processing delay and any buffering overhead.
266    /// Use this value to synchronize enhanced audio with other streams or to implement
267    /// delay compensation in your application.
268    ///
269    /// **Enhancement vs. VAD models:**
270    /// - For an enhancement model this is the latency of the enhanced audio: the number of
271    ///   samples by which the processed output lags behind the input.
272    /// - For a dedicated VAD model, the audio buffer is input-only and passes through unchanged.
273    ///   This delay is the VAD prediction latency: how many samples a speech decision from
274    ///   [`VadContext::is_speech_detected`](crate::VadContext::is_speech_detected) lags behind
275    ///   the input it describes. Use this value to line up VAD decisions with the input timeline.
276    ///
277    /// **Delay behavior:**
278    /// - **Before initialization:** Returns the base processing delay using the model's
279    ///   optimal frame size at its native sample rate
280    /// - **After initialization:** Returns the actual delay for your specific configuration,
281    ///   including any additional buffering introduced by non-optimal frame sizes
282    ///
283    /// **Important:** The delay value is always expressed in samples at the sample rate
284    /// you configured during `initialize`. To convert to time units:
285    /// `delay_ms = (delay_samples * 1000) / sample_rate`
286    ///
287    /// **Note:** Using frame sizes different from the optimal value returned by
288    /// `optimal_num_frames` will increase the delay beyond the model's base latency.
289    ///
290    /// # Returns
291    ///
292    /// Returns the delay in samples.
293    ///
294    /// # Example
295    ///
296    /// ```rust,no_run
297    /// # use aic_sdk::{Model, Processor};
298    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
299    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
300    /// # let processor = Processor::new(&model, &license_key)?;
301    /// # let processor_context = processor.processor_context();
302    /// let delay = processor_context.output_delay();
303    /// println!("Output delay: {} samples", delay);
304    /// # Ok::<(), aic_sdk::AicError>(())
305    /// ```
306    pub fn output_delay(&self) -> usize {
307        let mut delay: usize = 0;
308        // SAFETY:
309        // - `self.as_const_ptr()` is a valid pointer to a live processor context.
310        // - `delay` points to stack storage for output.
311        // - This function can be called from any thread, so we only borrow `&self`.
312        let error_code =
313            unsafe { aic_processor_context_get_output_delay(self.as_const_ptr(), &mut delay) };
314
315        // This should never fail. If it does, it's a bug in the SDK.
316        // `aic_get_output_delay` is documented to always succeed if given a valid processor pointer.
317        assert_success(
318            error_code,
319            "`aic_get_output_delay` failed. This is a bug, please open an issue on GitHub for further investigation.",
320        );
321
322        delay
323    }
324
325    /// Clears all internal state and buffers.
326    /// This also resets the VAD state associated with this processor.
327    ///
328    /// Call this when the audio stream is interrupted or when seeking
329    /// to prevent artifacts from previous audio content.
330    ///
331    /// The processor stays initialized to the configured settings.
332    ///
333    /// # Returns
334    ///
335    /// Returns `Ok(())` on success or an [`AicError`] if the reset fails.
336    ///
337    /// # Real-time safety
338    ///
339    /// Real-time safe. Can be called from audio processing threads.
340    ///
341    /// # Example
342    ///
343    /// ```rust,no_run
344    /// # use aic_sdk::{Model, Processor};
345    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
346    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
347    /// # let processor = Processor::new(&model, &license_key)?;
348    /// # let processor_context = processor.processor_context();
349    /// processor_context.reset()?;
350    /// # Ok::<(), aic_sdk::AicError>(())
351    /// ```
352    pub fn reset(&self) -> Result<(), AicError> {
353        // SAFETY:
354        // - `self.as_const_ptr()` is a valid pointer to a live processor context.
355        // - This function can be called from any thread, so we only borrow `&self`.
356        let error_code = unsafe { aic_processor_context_reset(self.as_const_ptr()) };
357        handle_error(error_code)
358    }
359
360    /// Replaces the bearer token on the running processor.
361    ///
362    /// Use this when your license key is a JWT and needs to be refreshed before it expires.
363    /// Audio processing continues uninterrupted, the context handle stays valid, and the new
364    /// token is used for all subsequent authentication against the ai-coustics backend.
365    ///
366    /// In-place updates are only supported when both the originally configured key and the
367    /// new token are JWTs. If either side is not, the call returns
368    /// [`AicError::TokenUpdateUnsupported`] and the existing token stays in use.
369    ///
370    /// # Arguments
371    ///
372    /// * `token` - The new JWT to install.
373    ///
374    /// # Returns
375    ///
376    /// Returns `Ok(())` on success or an [`AicError`] if the update fails.
377    ///
378    /// # Real-time safety
379    ///
380    /// This function is not real-time safe. It locks a mutex and allocates memory.
381    /// Avoid calling it from audio threads.
382    ///
383    /// # Example
384    ///
385    /// ```rust,no_run
386    /// # use aic_sdk::{Model, Processor};
387    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
388    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
389    /// let processor = Processor::new(&model, &license_key)?;
390    /// let processor_context = processor.processor_context();
391    /// let renewed_jwt = String::from("<JWT_BEARER_TOKEN>");
392    /// processor_context.update_bearer_token(&renewed_jwt)?;
393    /// # Ok::<(), aic_sdk::AicError>(())
394    /// ```
395    pub fn update_bearer_token(&self, token: &str) -> Result<(), AicError> {
396        let c_token = CString::new(token).map_err(|_| AicError::LicenseFormatInvalid)?;
397        // SAFETY:
398        // - `self.as_const_ptr()` is a valid pointer to a live processor context.
399        // - `c_token` is a null-terminated CString that outlives the call.
400        // - This function can be called from any thread.
401        let error_code = unsafe {
402            aic_processor_context_update_bearer_token(self.as_const_ptr(), c_token.as_ptr())
403        };
404        handle_error(error_code)
405    }
406}
407
408impl Drop for ProcessorContext {
409    fn drop(&mut self) {
410        if !self.inner.is_null() {
411            // SAFETY:
412            // - `self.inner` was allocated by the SDK and is still owned by this wrapper.
413            // - This function can be called from any thread; `drop` has exclusive
414            //   access to this context handle.
415            unsafe { aic_processor_context_destroy(self.inner) };
416        }
417    }
418}
419
420// Safety: The underlying C library should be thread-safe for individual ProcessorContext instances
421unsafe impl Send for ProcessorContext {}
422unsafe impl Sync for ProcessorContext {}
423
424/// High-level wrapper for the ai-coustics audio enhancement processor.
425///
426/// This struct provides a safe, Rust-friendly interface to the underlying C library.
427/// It handles memory management automatically and converts C-style error codes
428/// to Rust `Result` types.
429///
430/// # Example
431///
432/// ```rust,no_run
433/// use aic_sdk::{Model, ProcessorConfig, Processor};
434///
435/// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
436/// let model = Model::from_file("/path/to/model.aicmodel")?;
437/// let config = ProcessorConfig {
438///     num_channels: 2,
439///     num_frames: 1024,
440///     ..ProcessorConfig::optimal(&model)
441/// };
442///
443/// let mut processor = Processor::new(&model, &license_key)?.with_config(&config)?;
444///
445/// let mut audio_buffer = vec![0.0f32; config.num_channels as usize * config.num_frames];
446/// processor.process_interleaved(&mut audio_buffer)?;
447/// # Ok::<(), aic_sdk::AicError>(())
448/// ```
449pub struct Processor<'a> {
450    /// Raw pointer to the C processor structure
451    inner: *mut AicProcessor,
452    /// Configured number of channels
453    num_channels: Option<u16>,
454    /// Marker to tie the lifetime of the processor to the lifetime of the model's weights
455    marker: PhantomData<&'a [u8]>,
456}
457
458impl<'a> Processor<'a> {
459    /// Creates a new audio enhancement processor instance.
460    ///
461    /// Multiple processors can be created to process different audio streams simultaneously
462    /// or to switch between different enhancement algorithms during runtime.
463    ///
464    /// # Arguments
465    ///
466    /// * `model` - The loaded model instance
467    /// * `license_key` - license key for the ai-coustics SDK
468    ///   (generate your key at [developers.ai-coustics.com](https://developers.ai-coustics.com/))
469    ///
470    /// # Returns
471    ///
472    /// Returns a `Result` containing the new `Processor` instance or an [`AicError`] if creation fails.
473    ///
474    /// # Example
475    ///
476    /// ```rust,no_run
477    /// # use aic_sdk::{Model, Processor};
478    /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
479    /// let model = Model::from_file("/path/to/model.aicmodel")?;
480    /// let processor = Processor::new(&model, &license_key)?;
481    /// # Ok::<(), aic_sdk::AicError>(())
482    /// ```
483    pub fn new(model: &Model<'a>, license_key: &str) -> Result<Self, AicError> {
484        Self::create(model, license_key, None)
485    }
486
487    /// Creates a new audio enhancement processor instance with explicit
488    /// OpenTelemetry configuration.
489    ///
490    /// This overrides the SDK's environment-based telemetry defaults (e.g.
491    /// `AIC_SDK_OTEL_ENABLE`) for this processor.
492    ///
493    /// # Example
494    ///
495    /// ```rust,no_run
496    /// # use aic_sdk::{Model, OtelConfig, Processor};
497    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
498    /// let model = Model::from_file("/path/to/model.aicmodel")?;
499    /// let otel = OtelConfig::enabled();
500    ///
501    /// let processor = Processor::with_otel_config(&model, &license_key, &otel)?;
502    /// # Ok::<(), aic_sdk::AicError>(())
503    /// ```
504    pub fn with_otel_config(
505        model: &Model<'a>,
506        license_key: &str,
507        otel_config: &OtelConfig,
508    ) -> Result<Self, AicError> {
509        Self::create(model, license_key, Some(otel_config))
510    }
511
512    fn create(
513        model: &Model<'a>,
514        license_key: &str,
515        otel_config: Option<&OtelConfig>,
516    ) -> Result<Self, AicError> {
517        // Set the wrapper ID as soon as the user attempts to instantiate a processor
518        crate::set_wrapper_id();
519
520        // Session ID must outlive the FFI call so its pointer stays valid.
521        let c_session_id = otel_config
522            .and_then(|o| o.session_id.as_deref())
523            .map(CString::new)
524            .transpose()
525            .map_err(|_| AicError::Internal)?;
526
527        let c_otel = otel_config.map(|o| AicOtelConfig {
528            enable: o.enable,
529            session_id: c_session_id.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
530            export_interval_ms: o.export_interval_ms,
531        });
532        let c_otel_ptr = c_otel
533            .as_ref()
534            .map_or(ptr::null(), |o| o as *const AicOtelConfig);
535
536        let mut processor_ptr: *mut AicProcessor = ptr::null_mut();
537        let c_license_key =
538            CString::new(license_key).map_err(|_| AicError::LicenseFormatInvalid)?;
539
540        // SAFETY:
541        // - `processor_ptr` points to stack storage for output.
542        // - `model` is a valid SDK model pointer for the duration of the call.
543        // - `c_license_key` is a null-terminated CString.
544        // - `c_otel_ptr` is either null or points to a valid `AicOtelConfig` whose
545        //   `session_id` field (if non-null) outlives this call.
546        // - This function is not thread-safe, but the output pointer is local to
547        //   this call and no processor handle exists until it returns.
548        let error_code = unsafe {
549            aic_processor_create(
550                &mut processor_ptr,
551                model.as_const_ptr(),
552                c_license_key.as_ptr(),
553                c_otel_ptr,
554            )
555        };
556
557        handle_error(error_code)?;
558
559        // This should never happen if the C library is well-behaved, but let's be defensive
560        assert!(
561            !processor_ptr.is_null(),
562            "C library returned success but null pointer"
563        );
564
565        Ok(Self {
566            inner: processor_ptr,
567            num_channels: None,
568            marker: PhantomData,
569        })
570    }
571
572    /// Initializes the processor with the given configuration.
573    ///
574    /// This is a convenience method that calls [`Processor::initialize`] internally and returns `self`.
575    /// The processor is immediately ready to process audio after calling this method, so you don't
576    /// need to call [`Processor::initialize`] separately.
577    ///
578    /// # Arguments
579    ///
580    /// * `config` - Audio processing configuration
581    ///
582    /// # Returns
583    ///
584    /// Returns `Ok(Self)` with the initialized processor, or an [`AicError`] if initialization fails.
585    ///
586    /// # Example
587    ///
588    /// ```rust,no_run
589    /// # use aic_sdk::{Model, Processor, ProcessorConfig};
590    /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
591    /// let model = Model::from_file("/path/to/model.aicmodel")?;
592    /// let config = ProcessorConfig::optimal(&model).with_num_channels(2);
593    ///
594    /// let mut processor = Processor::new(&model, &license_key)?.with_config(&config)?;
595    ///
596    /// // Processor is ready to use - no need to call initialize()
597    /// let mut audio = vec![0.0f32; config.num_channels as usize * config.num_frames];
598    /// processor.process_interleaved(&mut audio)?;
599    /// # Ok::<(), aic_sdk::AicError>(())
600    /// ```
601    pub fn with_config(mut self, config: &ProcessorConfig) -> Result<Self, AicError> {
602        self.initialize(config)?;
603        Ok(self)
604    }
605
606    /// Creates a [ProcessorContext] instance.
607    /// This can be used to control all parameters and other settings of the processor.
608    ///
609    /// # Example
610    ///
611    /// ```rust,no_run
612    /// # use aic_sdk::{Model, Processor};
613    /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
614    /// let model = Model::from_file("/path/to/model.aicmodel")?;
615    /// let processor = Processor::new(&model, &license_key)?;
616    /// let processor_context = processor.processor_context();
617    /// # Ok::<(), aic_sdk::AicError>(())
618    /// ```
619    pub fn processor_context(&self) -> ProcessorContext {
620        let mut processor_context: *mut AicProcessorContext = ptr::null_mut();
621
622        // SAFETY:
623        // - `processor_context` is valid output storage.
624        // - `self.as_const_ptr()` is a live processor pointer.
625        // - This function can be called from any thread, so we only borrow `&self`.
626        let error_code =
627            unsafe { aic_processor_context_create(&mut processor_context, self.as_const_ptr()) };
628
629        // This should never fail
630        assert!(handle_error(error_code).is_ok());
631
632        // This should never happen if the C library is well-behaved, but let's be defensive
633        assert!(
634            !processor_context.is_null(),
635            "C library returned success but null pointer"
636        );
637
638        ProcessorContext::new(processor_context)
639    }
640
641    /// Creates a [Voice Activity Detector Context](crate::vad::VadContext) instance.
642    /// All handles created from a given processor reference the same VAD instance.
643    ///
644    /// # Example
645    ///
646    /// ```rust,no_run
647    /// # use aic_sdk::{Model, Processor};
648    /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
649    /// let model = Model::from_file("/path/to/model.aicmodel")?;
650    /// let processor = Processor::new(&model, &license_key)?;
651    /// let vad = processor.vad_context();
652    /// # Ok::<(), aic_sdk::AicError>(())
653    /// ```
654    pub fn vad_context(&self) -> crate::VadContext {
655        let mut vad_ptr: *mut AicVadContext = ptr::null_mut();
656
657        // SAFETY:
658        // - `vad_ptr` is valid output storage.
659        // - `self.as_const_ptr()` is a live processor pointer.
660        // - This function can be called from any thread and may run while the
661        //   processor is in use, so we only borrow `&self`.
662        let error_code = unsafe { aic_vad_context_create(&mut vad_ptr, self.as_const_ptr()) };
663
664        // This should never fail
665        assert!(handle_error(error_code).is_ok());
666
667        // This should never happen if the C library is well-behaved, but let's be defensive
668        assert!(
669            !vad_ptr.is_null(),
670            "C library returned success but null pointer"
671        );
672
673        crate::vad::VadContext::new(vad_ptr)
674    }
675
676    /// Configures the processor for specific audio settings.
677    ///
678    /// This function must be called before processing any audio.
679    /// For the lowest delay use the sample rate and frame size returned by
680    /// [`Model::optimal_sample_rate`] and [`Model::optimal_num_frames`].
681    ///
682    /// # Arguments
683    ///
684    /// * `config` - Audio processing configuration
685    ///
686    /// # Returns
687    ///
688    /// Returns `Ok(())` on success or an [`AicError`] if initialization fails.
689    ///
690    /// # Warning
691    /// Do not call from audio processing threads as this allocates memory.
692    ///
693    /// # Note
694    /// All channels are mixed to mono for processing. To process channels
695    /// independently, create separate [`Processor`] instances.
696    ///
697    /// # Example
698    ///
699    /// ```rust,no_run
700    /// # use aic_sdk::{Model, Processor, ProcessorConfig};
701    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
702    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
703    /// # let mut processor = Processor::new(&model, &license_key)?;
704    /// let config = ProcessorConfig::optimal(&model);
705    /// processor.initialize(&config)?;
706    /// # Ok::<(), aic_sdk::AicError>(())
707    /// ```
708    pub fn initialize(&mut self, config: &ProcessorConfig) -> Result<(), AicError> {
709        // SAFETY:
710        // - `self.inner` is a valid pointer to a live processor.
711        // - This function is not thread-safe, so we borrow `&mut self`.
712        let error_code = unsafe {
713            aic_processor_initialize(
714                self.inner,
715                config.sample_rate,
716                config.num_channels,
717                config.num_frames,
718                config.allow_variable_frames,
719            )
720        };
721
722        handle_error(error_code)?;
723        self.num_channels = Some(config.num_channels);
724        Ok(())
725    }
726
727    /// Processes audio with separate buffers for each channel (planar layout).
728    ///
729    /// Enhances speech in the provided audio buffers in-place.
730    ///
731    /// **Memory Layout:**
732    /// - Separate buffer for each channel
733    /// - Each buffer contains `num_frames` floats
734    /// - Maximum of 16 channels supported
735    /// - Example for 2 channels, 4 frames:
736    ///   ```text
737    ///   audio[0] -> [ch0_f0, ch0_f1, ch0_f2, ch0_f3]
738    ///   audio[1] -> [ch1_f0, ch1_f1, ch1_f2, ch1_f3]
739    ///   ```
740    ///
741    /// The function accepts any type of collection of `f32` values that implements `as_mut`, e.g.:
742    /// - `[vec![0.0; 128]; 2]`
743    /// - `[[0.0; 128]; 2]`
744    /// - `[&mut ch1, &mut ch2]`
745    ///
746    /// # Arguments
747    ///
748    /// * `audio` - Array of mutable channel buffer slices to be enhanced in-place.
749    ///             Each channel buffer must be exactly of size `num_frames`,
750    ///             or if `allow_variable_frames` was enabled, less than the initialization value.
751    ///
752    /// # Notes
753    ///
754    /// - All channels are mixed to mono for processing. To process channels
755    ///   independently, create separate processor instances.
756    /// - Maximum supported number of channels is 16. Exceeding this will return an error.
757    ///
758    /// # Returns
759    ///
760    /// Returns `Ok(())` on success or an [`AicError`] if processing fails.
761    ///
762    /// # Real-time safety
763    ///
764    /// Real-time safe. Can be called from audio processing threads.
765    ///
766    /// # Example
767    ///
768    /// ```rust,no_run
769    /// # use aic_sdk::{Model, Processor, ProcessorConfig};
770    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
771    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
772    /// # let mut processor = Processor::new(&model, &license_key)?;
773    /// let config = ProcessorConfig::optimal(&model).with_num_channels(2);
774    /// processor.initialize(&config)?;
775    /// let mut audio = vec![vec![0.0f32; config.num_frames]; config.num_channels as usize];
776    /// processor.process_planar(&mut audio)?;
777    /// # Ok::<(), aic_sdk::AicError>(())
778    /// ```
779    #[allow(clippy::doc_overindented_list_items)]
780    pub fn process_planar<V: AsMut<[f32]>>(&mut self, audio: &mut [V]) -> Result<(), AicError> {
781        const MAX_CHANNELS: u16 = 16;
782
783        let Some(num_channels) = self.num_channels else {
784            return Err(AicError::ProcessorNotInitialized);
785        };
786
787        if audio.len() != num_channels as usize {
788            return Err(AicError::AudioConfigMismatch);
789        }
790
791        if num_channels > MAX_CHANNELS {
792            return Err(AicError::AudioConfigUnsupported);
793        }
794
795        let num_frames = if audio.is_empty() {
796            0
797        } else {
798            audio[0].as_mut().len()
799        };
800
801        let mut audio_ptrs = [std::ptr::null_mut::<f32>(); MAX_CHANNELS as usize];
802        for (i, channel) in audio.iter_mut().enumerate() {
803            // Check that all channels have the same number of frames
804            if channel.as_mut().len() != num_frames {
805                return Err(AicError::AudioConfigMismatch);
806            }
807            audio_ptrs[i] = channel.as_mut().as_mut_ptr();
808        }
809
810        // SAFETY:
811        // - `self.inner` is a valid pointer to a live processor.
812        // - `audio_ptrs` holds `num_channels` valid, writable pointers with `num_frames` samples each.
813        // - This function is not thread-safe, so we borrow `&mut self`.
814        let error_code = unsafe {
815            aic_processor_process_planar(self.inner, audio_ptrs.as_ptr(), num_channels, num_frames)
816        };
817
818        handle_error(error_code)
819    }
820
821    /// Processes audio with interleaved channel data.
822    ///
823    /// Enhances speech in the provided audio buffer in-place.
824    ///
825    /// **Memory Layout:**
826    /// - Single contiguous buffer with samples alternating between channels
827    /// - Buffer size: `num_channels` * `num_frames` floats
828    /// - Example for 2 channels, 4 frames:
829    ///   ```text
830    ///   audio -> [ch0_f0, ch1_f0, ch0_f1, ch1_f1, ch0_f2, ch1_f2, ch0_f3, ch1_f3]
831    ///   ```
832    ///
833    /// # Arguments
834    ///
835    /// * `audio` - Interleaved audio buffer to be enhanced in-place.
836    ///             Must be exactly of size `num_channels` * `num_frames`,
837    ///             or if `allow_variable_frames` was enabled, less than the initialization value per channel.
838    ///
839    /// # Note
840    ///
841    /// All channels are mixed to mono for processing. To process channels
842    /// independently, create separate processor instances.
843    ///
844    /// # Returns
845    ///
846    /// Returns `Ok(())` on success or an [`AicError`] if processing fails.
847    ///
848    /// # Real-time safety
849    ///
850    /// Real-time safe. Can be called from audio processing threads.
851    ///
852    /// # Example
853    ///
854    /// ```rust,no_run
855    /// # use aic_sdk::{Model, Processor, ProcessorConfig};
856    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
857    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
858    /// # let mut processor = Processor::new(&model, &license_key)?;
859    /// let config = ProcessorConfig::optimal(&model).with_num_channels(2);
860    /// processor.initialize(&config)?;
861    /// let mut audio = vec![0.0f32; config.num_channels as usize * config.num_frames];
862    /// processor.process_interleaved(&mut audio)?;
863    /// # Ok::<(), aic_sdk::AicError>(())
864    /// ```
865    #[allow(clippy::doc_overindented_list_items)]
866    pub fn process_interleaved(&mut self, audio: &mut [f32]) -> Result<(), AicError> {
867        let Some(num_channels) = self.num_channels else {
868            return Err(AicError::ProcessorNotInitialized);
869        };
870
871        if !audio.len().is_multiple_of(num_channels as usize) {
872            return Err(AicError::AudioConfigMismatch);
873        }
874
875        let num_frames = audio.len() / num_channels as usize;
876
877        // SAFETY:
878        // - `self.inner` is a valid pointer to a live processor.
879        // - `audio` points to a contiguous f32 slice of length `num_channels * num_frames`.
880        // - This function is not thread-safe, so we borrow `&mut self`.
881        let error_code = unsafe {
882            aic_processor_process_interleaved(
883                self.inner,
884                audio.as_mut_ptr(),
885                num_channels,
886                num_frames,
887            )
888        };
889
890        handle_error(error_code)
891    }
892
893    /// Processes audio with sequential channel data.
894    ///
895    /// Enhances speech in the provided audio buffer in-place.
896    ///
897    /// **Memory Layout:**
898    /// - Single contiguous buffer with all samples for each channel stored sequentially
899    /// - Buffer size: `num_channels` * `num_frames` floats
900    /// - Example for 2 channels, 4 frames:
901    ///   ```text
902    ///   audio -> [ch0_f0, ch0_f1, ch0_f2, ch0_f3, ch1_f0, ch1_f1, ch1_f2, ch1_f3]
903    ///   ```
904    ///
905    /// # Arguments
906    ///
907    /// * `audio` - Sequential audio buffer to be enhanced in-place.
908    ///             Must be exactly of size `num_channels` * `num_frames`,
909    ///             or if `allow_variable_frames` was enabled, less than the initialization value per channel.
910    /// # Note
911    ///
912    /// All channels are mixed to mono for processing. To process channels
913    /// independently, create separate processor instances.
914    ///
915    /// # Returns
916    ///
917    /// Returns `Ok(())` on success or an [`AicError`] if processing fails.
918    ///
919    /// # Real-time safety
920    ///
921    /// Real-time safe. Can be called from audio processing threads.
922    ///
923    /// # Example
924    ///
925    /// ```rust,no_run
926    /// # use aic_sdk::{Model, Processor, ProcessorConfig};
927    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
928    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
929    /// # let mut processor = Processor::new(&model, &license_key)?;
930    /// let config = ProcessorConfig::optimal(&model).with_num_channels(2);
931    /// processor.initialize(&config)?;
932    /// let mut audio = vec![0.0f32; config.num_channels as usize * config.num_frames];
933    /// processor.process_sequential(&mut audio)?;
934    /// # Ok::<(), aic_sdk::AicError>(())
935    /// ```
936    #[allow(clippy::doc_overindented_list_items)]
937    pub fn process_sequential(&mut self, audio: &mut [f32]) -> Result<(), AicError> {
938        let Some(num_channels) = self.num_channels else {
939            return Err(AicError::ProcessorNotInitialized);
940        };
941
942        if !audio.len().is_multiple_of(num_channels as usize) {
943            return Err(AicError::AudioConfigMismatch);
944        }
945
946        let num_frames = audio.len() / num_channels as usize;
947
948        // SAFETY:
949        // - `self.inner` is a valid pointer to a live, initialized processor.
950        // - `audio` points to a contiguous f32 slice of length `num_channels * num_frames`.
951        // - This function is not thread-safe, so we borrow `&mut self`.
952        let error_code = unsafe {
953            aic_processor_process_sequential(
954                self.inner,
955                audio.as_mut_ptr(),
956                num_channels,
957                num_frames,
958            )
959        };
960
961        handle_error(error_code)
962    }
963
964    fn as_const_ptr(&self) -> *const AicProcessor {
965        self.inner as *const AicProcessor
966    }
967}
968
969impl<'a> Drop for Processor<'a> {
970    fn drop(&mut self) {
971        if !self.inner.is_null() {
972            // SAFETY:
973            // - `self.inner` was allocated by the SDK and is still owned by this wrapper.
974            // - This function is not thread-safe with concurrent processor use, but
975            //   `drop` has exclusive access to `self`.
976            unsafe { aic_processor_destroy(self.inner) };
977        }
978    }
979}
980
981// SAFETY: Everything in Processor is Send, with the exception of the inner raw pointer.
982// The Processor only uses the raw pointer according to the safety contracts of the
983// unsafe APIs that require the pointer, and the Processor does not expose access to the
984// raw pointer in any of its methods. Therefore, it safe to implement Send for Processor.
985unsafe impl<'a> Send for Processor<'a> {}
986
987// SAFETY: Processor does not expose any interior mutability, and all unsafe APIs that make use of
988// the inner raw pointer are only used in methods that take &mut self, which upholds the thread safety
989// contracts required by the unsafe APIs. Therefore, it is safe to implement Sync for Processor.
990unsafe impl<'a> Sync for Processor<'a> {}
991
992#[cfg(test)]
993mod tests {
994    use super::*;
995    use std::{
996        fs,
997        path::{Path, PathBuf},
998        sync::{Mutex, OnceLock},
999    };
1000
1001    fn download_lock() -> &'static Mutex<()> {
1002        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1003        LOCK.get_or_init(|| Mutex::new(()))
1004    }
1005
1006    fn find_existing_model(target_dir: &Path) -> Option<PathBuf> {
1007        let entries = fs::read_dir(target_dir).ok()?;
1008        for entry in entries.flatten() {
1009            let path = entry.path();
1010            if path
1011                .file_name()
1012                .and_then(|n| n.to_str())
1013                .map(|name| name.contains("rook_s_48khz") && name.ends_with(".aicmodel"))
1014                .unwrap_or(false)
1015                && path.is_file()
1016            {
1017                return Some(path);
1018            }
1019        }
1020        None
1021    }
1022
1023    /// Downloads the default test model `rook-s-48khz` into the crate's `target/` directory.
1024    /// Returns the path to the downloaded model file.
1025    fn get_rook_s_48khz() -> Result<PathBuf, AicError> {
1026        let target_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target");
1027
1028        if let Some(existing) = find_existing_model(&target_dir) {
1029            return Ok(existing);
1030        }
1031
1032        let _guard = download_lock().lock().unwrap();
1033        if let Some(existing) = find_existing_model(&target_dir) {
1034            return Ok(existing);
1035        }
1036
1037        if cfg!(feature = "download-model") {
1038            Model::download("rook-s-48khz", target_dir)
1039        } else {
1040            panic!(
1041                "Model `rook-s-48khz` not found in {} and `download-model` feature is disabled",
1042                target_dir.display()
1043            );
1044        }
1045    }
1046
1047    fn load_test_model() -> Result<(Model<'static>, String), AicError> {
1048        let license_key = std::env::var("AIC_SDK_LICENSE")
1049            .expect("AIC_SDK_LICENSE environment variable must be set for tests");
1050
1051        let model_path = get_rook_s_48khz()?;
1052        let model = Model::from_file(&model_path)?;
1053
1054        Ok((model, license_key))
1055    }
1056
1057    #[test]
1058    fn model_creation_and_basic_operations() {
1059        dbg!(crate::get_sdk_version());
1060        dbg!(crate::get_compatible_model_version());
1061
1062        let (model, license_key) = load_test_model().unwrap();
1063        let config = ProcessorConfig::optimal(&model).with_num_channels(2);
1064
1065        let mut processor = Processor::new(&model, &license_key)
1066            .unwrap()
1067            .with_config(&config)
1068            .unwrap();
1069
1070        let num_channels = config.num_channels as usize;
1071        let mut audio = vec![vec![0.0f32; config.num_frames]; num_channels];
1072        let mut audio_refs: Vec<&mut [f32]> =
1073            audio.iter_mut().map(|ch| ch.as_mut_slice()).collect();
1074
1075        processor.process_planar(&mut audio_refs).unwrap();
1076    }
1077
1078    #[test]
1079    fn process_interleaved_fixed_frames() {
1080        let (model, license_key) = load_test_model().unwrap();
1081        let config = ProcessorConfig::optimal(&model).with_num_channels(2);
1082
1083        let mut processor = Processor::new(&model, &license_key)
1084            .unwrap()
1085            .with_config(&config)
1086            .unwrap();
1087
1088        let num_channels = config.num_channels as usize;
1089        let mut audio = vec![0.0f32; num_channels * config.num_frames];
1090        processor.process_interleaved(&mut audio).unwrap();
1091    }
1092
1093    #[test]
1094    fn process_planar_fixed_frames() {
1095        let (model, license_key) = load_test_model().unwrap();
1096        let config = ProcessorConfig::optimal(&model).with_num_channels(2);
1097
1098        let mut processor = Processor::new(&model, &license_key)
1099            .unwrap()
1100            .with_config(&config)
1101            .unwrap();
1102
1103        let mut left = vec![0.0f32; config.num_frames];
1104        let mut right = vec![0.0f32; config.num_frames];
1105        let mut audio = [left.as_mut_slice(), right.as_mut_slice()];
1106        processor.process_planar(&mut audio).unwrap();
1107    }
1108
1109    #[test]
1110    fn process_sequential_fixed_frames() {
1111        let (model, license_key) = load_test_model().unwrap();
1112        let config = ProcessorConfig::optimal(&model).with_num_channels(2);
1113
1114        let mut processor = Processor::new(&model, &license_key)
1115            .unwrap()
1116            .with_config(&config)
1117            .unwrap();
1118
1119        let num_channels = config.num_channels as usize;
1120        let mut audio = vec![0.0f32; num_channels * config.num_frames];
1121        processor.process_sequential(&mut audio).unwrap();
1122    }
1123
1124    #[test]
1125    fn process_interleaved_variable_frames() {
1126        let (model, license_key) = load_test_model().unwrap();
1127        let config = ProcessorConfig::optimal(&model)
1128            .with_num_channels(2)
1129            .with_allow_variable_frames(true);
1130
1131        let mut processor = Processor::new(&model, &license_key)
1132            .unwrap()
1133            .with_config(&config)
1134            .unwrap();
1135
1136        let num_channels = config.num_channels as usize;
1137        let mut audio = vec![0.0f32; num_channels * config.num_frames];
1138        processor.process_interleaved(&mut audio).unwrap();
1139
1140        let mut audio = vec![0.0f32; num_channels * 20];
1141        processor.process_interleaved(&mut audio).unwrap();
1142    }
1143
1144    #[test]
1145    fn process_planar_variable_frames() {
1146        let (model, license_key) = load_test_model().unwrap();
1147        let config = ProcessorConfig::optimal(&model)
1148            .with_num_channels(2)
1149            .with_allow_variable_frames(true);
1150
1151        let mut processor = Processor::new(&model, &license_key)
1152            .unwrap()
1153            .with_config(&config)
1154            .unwrap();
1155
1156        let mut left = vec![0.0f32; config.num_frames];
1157        let mut right = vec![0.0f32; config.num_frames];
1158        let mut audio = [left.as_mut_slice(), right.as_mut_slice()];
1159        processor.process_planar(&mut audio).unwrap();
1160
1161        let mut left = vec![0.0f32; 20];
1162        let mut right = vec![0.0f32; 20];
1163        let mut audio = [left.as_mut_slice(), right.as_mut_slice()];
1164        processor.process_planar(&mut audio).unwrap();
1165    }
1166
1167    #[test]
1168    fn process_sequential_variable_frames() {
1169        let (model, license_key) = load_test_model().unwrap();
1170        let config = ProcessorConfig::optimal(&model)
1171            .with_num_channels(2)
1172            .with_allow_variable_frames(true);
1173
1174        let mut processor = Processor::new(&model, &license_key)
1175            .unwrap()
1176            .with_config(&config)
1177            .unwrap();
1178
1179        let num_channels = config.num_channels as usize;
1180        let mut audio = vec![0.0f32; num_channels * config.num_frames];
1181        processor.process_sequential(&mut audio).unwrap();
1182
1183        let mut audio = vec![0.0f32; num_channels * 20];
1184        processor.process_sequential(&mut audio).unwrap();
1185    }
1186
1187    #[test]
1188    fn process_interleaved_variable_frames_fails_without_allow_variable_frames() {
1189        let (model, license_key) = load_test_model().unwrap();
1190        let config = ProcessorConfig::optimal(&model).with_num_channels(2);
1191
1192        let mut processor = Processor::new(&model, &license_key)
1193            .unwrap()
1194            .with_config(&config)
1195            .unwrap();
1196
1197        let num_channels = config.num_channels as usize;
1198        let mut audio = vec![0.0f32; num_channels * config.num_frames];
1199        processor.process_interleaved(&mut audio).unwrap();
1200
1201        let mut audio = vec![0.0f32; num_channels * 20];
1202        let result = processor.process_interleaved(&mut audio);
1203        assert_eq!(result, Err(AicError::AudioConfigMismatch));
1204    }
1205
1206    #[test]
1207    fn process_planar_variable_frames_fails_without_allow_variable_frames() {
1208        let (model, license_key) = load_test_model().unwrap();
1209        let config = ProcessorConfig::optimal(&model).with_num_channels(2);
1210
1211        let mut processor = Processor::new(&model, &license_key)
1212            .unwrap()
1213            .with_config(&config)
1214            .unwrap();
1215
1216        let mut left = vec![0.0f32; config.num_frames];
1217        let mut right = vec![0.0f32; config.num_frames];
1218        let mut audio = [left.as_mut_slice(), right.as_mut_slice()];
1219        processor.process_planar(&mut audio).unwrap();
1220
1221        let mut left = vec![0.0f32; 20];
1222        let mut right = vec![0.0f32; 20];
1223        let mut audio = [left.as_mut_slice(), right.as_mut_slice()];
1224        let result = processor.process_planar(&mut audio);
1225        assert_eq!(result, Err(AicError::AudioConfigMismatch));
1226    }
1227
1228    #[test]
1229    fn process_sequential_variable_frames_fails_without_allow_variable_frames() {
1230        let (model, license_key) = load_test_model().unwrap();
1231        let config = ProcessorConfig::optimal(&model).with_num_channels(2);
1232
1233        let mut processor = Processor::new(&model, &license_key)
1234            .unwrap()
1235            .with_config(&config)
1236            .unwrap();
1237
1238        let num_channels = config.num_channels as usize;
1239        let mut audio = vec![0.0f32; num_channels * config.num_frames];
1240        processor.process_sequential(&mut audio).unwrap();
1241
1242        let mut audio = vec![0.0f32; num_channels * 20];
1243        let result = processor.process_sequential(&mut audio);
1244        assert_eq!(result, Err(AicError::AudioConfigMismatch));
1245    }
1246
1247    #[test]
1248    fn model_can_be_dropped_after_creating_processor() {
1249        let (model, license_key) = load_test_model().unwrap();
1250        let config = ProcessorConfig::optimal(&model).with_num_channels(2);
1251
1252        let mut processor = Processor::new(&model, &license_key)
1253            .unwrap()
1254            .with_config(&config)
1255            .unwrap();
1256        drop(model); // Inside of the SDK an Arc-Pointer to `Model` is stored in Processor, so it won't be de-allocated
1257
1258        let num_channels = config.num_channels as usize;
1259        let mut audio = vec![vec![0.0f32; config.num_frames]; num_channels];
1260        let mut audio_refs: Vec<&mut [f32]> =
1261            audio.iter_mut().map(|ch| ch.as_mut_slice()).collect();
1262
1263        processor.process_planar(&mut audio_refs).unwrap();
1264    }
1265
1266    #[test]
1267    fn processor_is_send_and_sync() {
1268        // Compile-time check that Processor implements Send and Sync.
1269        // This ensures the processor can be safely moved to another thread.
1270        fn assert_send<T: Send>() {}
1271        fn assert_sync<T: Send>() {}
1272
1273        assert_send::<Processor>();
1274        assert_sync::<Processor>();
1275    }
1276
1277    struct MyModel {
1278        _model: Model<'static>,
1279        _processor: Processor<'static>,
1280    }
1281
1282    impl MyModel {
1283        pub fn new() -> Self {
1284            let (model, license_key) = load_test_model().unwrap();
1285            let processor = Processor::new(&model, &license_key)
1286                .unwrap()
1287                .with_config(&ProcessorConfig::optimal(&model))
1288                .unwrap();
1289            MyModel {
1290                _model: model,
1291                _processor: processor,
1292            }
1293        }
1294    }
1295
1296    #[test]
1297    fn can_create_self_referential_structs_with_statics() {
1298        let _model = MyModel::new();
1299    }
1300}
1301
1302#[doc(hidden)]
1303mod _compile_fail_tests {
1304    //! Compile-fail regression: a `Processor`'s model buffer must not be dropped before the processor.
1305    //!
1306    //! ```rust,compile_fail
1307    //! use aic_sdk::{Model, Processor, ProcessorConfig};
1308    //!
1309    //! fn main() {
1310    //!     let buffer = vec![0u8; 64];
1311    //!     let model = Model::from_buffer(&buffer).unwrap();
1312    //!     let config = ProcessorConfig::optimal(&model).with_num_channels(2);
1313    //!
1314    //!     let mut processor = Processor::new(&model, "license")
1315    //!         .unwrap()
1316    //!         .with_config(&config)
1317    //!         .unwrap();
1318    //!
1319    //!     drop(model); // Model can be dropped without issues
1320    //!
1321    //!     drop(buffer); // This should fail to compile
1322    //!
1323    //!     let num_channels = config.num_channels as usize;
1324    //!     let mut audio = vec![vec![0.0f32; config.num_frames]; num_channels];
1325    //!     processor.process_planar(&mut audio).unwrap();
1326    //! }
1327    //! ```
1328}