Skip to main content

aic_sdk/
processor.rs

1use crate::{energy_vad::EnergyVadContext, 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/// [`Vad::initialize`](crate::Vad::initialize) and
9/// [`Collector::initialize`](crate::Collector::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 samples passed to each [`Processor::process`],
18    /// [`Vad::process`](crate::Vad::process) or
19    /// [`Collector::buffer`](crate::Collector::buffer) call (the maximum, if
20    /// `variable_block_size` is `true`).
21    /// Note that using a non-optimal block size increases latency.
22    pub block_size: usize,
23    /// If `true`, permits shorter calls at the cost of added delay.
24    /// Calls larger than `block_size` are always rejected.
25    pub variable_block_size: bool,
26}
27
28impl ProcessorConfig {
29    /// Returns a [`ProcessorConfig`] pre-filled with the model's optimal sample rate and block size.
30    ///
31    /// `variable_block_size` will be set to `false`. Enable variable block sizes
32    /// by using the builder pattern.
33    ///
34    /// ```rust,no_run
35    /// # use aic_sdk::{Model, ProcessorConfig, Processor};
36    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
37    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
38    /// # let processor = Processor::new(&model, &license_key)?;
39    /// let config = ProcessorConfig::optimal(&model).with_variable_block_size(true);
40    /// # Ok::<(), aic_sdk::AicError>(())
41    /// ```
42    ///
43    /// If you need to configure a non-optimal sample rate or block size,
44    /// construct the [`ProcessorConfig`] struct directly. For example:
45    /// ```rust,no_run
46    /// # use aic_sdk::{Model, ProcessorConfig};
47    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
48    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
49    /// let config = ProcessorConfig {
50    ///     sample_rate: 44100,
51    ///     block_size: model.optimal_block_size(44100),
52    ///     variable_block_size: true,
53    /// };
54    /// # Ok::<(), aic_sdk::AicError>(())
55    /// ```
56    pub fn optimal(model: &Model) -> Self {
57        let sample_rate = model.optimal_sample_rate();
58        let block_size = model.optimal_block_size(sample_rate);
59        ProcessorConfig {
60            sample_rate,
61            block_size,
62            variable_block_size: false,
63        }
64    }
65
66    /// Enables or disables variable block size support.
67    ///
68    /// When enabled, permits processing calls shorter than `block_size` at the cost of
69    /// added latency.
70    ///
71    /// # Arguments
72    ///
73    /// * `variable_block_size` - `true` to enable variable block sizes, `false` for fixed size
74    pub fn with_variable_block_size(mut self, variable_block_size: bool) -> Self {
75        self.variable_block_size = variable_block_size;
76        self
77    }
78}
79
80/// Configurable parameters for audio enhancement
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub enum ProcessorParameter {
83    /// Controls whether audio processing is bypassed while preserving algorithmic delay.
84    ///
85    /// When enabled, the input audio passes through unmodified, but the output is still
86    /// delayed by the same amount as during normal processing. This ensures seamless
87    /// transitions when toggling enhancement on/off without audible clicks or timing shifts.
88    ///
89    /// **Range:** 0.0 to 1.0
90    /// - **0.0:** Enhancement active (normal processing)
91    /// - **1.0:** Bypass enabled (latency-compensated passthrough)
92    ///
93    /// **Default:** 0.0
94    Bypass,
95    /// A tunable parameter to optimize for specific STT engines, deployment environments,
96    /// and user experience requirements.
97    ///
98    /// The exact behavior depends on the active model:
99    /// - **Quail Models:** Controls how aggressively the model suppresses noise. When used
100    ///   with Quail Voice Focus, it also suppresses background and competing speech.
101    /// - **Rook Models:** Controls the mixback and therefore the intensity of the
102    ///   enhancement.
103    ///
104    /// **Range:** 0.0 to 1.0
105    EnhancementLevel,
106}
107
108impl From<ProcessorParameter> for AicProcessorParameter::Type {
109    fn from(parameter: ProcessorParameter) -> Self {
110        match parameter {
111            ProcessorParameter::Bypass => AIC_PROCESSOR_PARAMETER_BYPASS,
112            ProcessorParameter::EnhancementLevel => AIC_PROCESSOR_PARAMETER_ENHANCEMENT_LEVEL,
113        }
114    }
115}
116
117/// OpenTelemetry configuration for a [`Processor`] or [`Vad`](crate::Vad).
118///
119/// Pass to [`Processor::with_otel_config`] or [`Vad::with_otel_config`](crate::Vad::with_otel_config)
120/// to control telemetry on a per-instance basis. When no [`OtelConfig`] is provided (e.g. when
121/// using [`Processor::new`]), telemetry is configured according to the runtime environment
122/// (e.g. the `AIC_SDK_OTEL_ENABLE` environment variable).
123#[derive(Debug, Clone, PartialEq, Eq, Hash)]
124pub struct OtelConfig {
125    /// Whether to enable OpenTelemetry telemetry.
126    ///
127    /// Overrides the `AIC_SDK_OTEL_ENABLE` environment variable.
128    pub enable: bool,
129    /// Optional session ID for telemetry. If `None`, a random session ID is generated.
130    pub session_id: Option<String>,
131    /// OpenTelemetry metric export interval in milliseconds.
132    ///
133    /// Set to `0` to use the SDK default of 60 000 ms.
134    pub export_interval_ms: u32,
135}
136
137impl OtelConfig {
138    /// Returns an [`OtelConfig`] with telemetry disabled.
139    pub fn disabled() -> Self {
140        Self {
141            enable: false,
142            session_id: None,
143            export_interval_ms: 0,
144        }
145    }
146
147    /// Returns an [`OtelConfig`] with telemetry enabled.
148    pub fn enabled() -> Self {
149        Self {
150            enable: true,
151            session_id: None,
152            export_interval_ms: 0,
153        }
154    }
155
156    /// Returns an [`OtelConfig`] with telemetry enabled and the provided session ID.
157    pub fn with_session_id(session_id: impl Into<String>) -> Self {
158        Self {
159            enable: true,
160            session_id: Some(session_id.into()),
161            export_interval_ms: 0,
162        }
163    }
164}
165
166/// Thread-safe control handle for a [`Processor`].
167///
168/// Create one with [`Processor::context`]. Every method on this type maps to an SDK
169/// function that can be called from any thread, so a context can be moved to another thread to
170/// read and write parameters, query the audio delay, or reset the processor while audio is being
171/// processed elsewhere.
172///
173/// Dropping the context does not destroy the processor it came from, and multiple contexts can be
174/// created from the same processor.
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_ptr(&self) -> *const AicProcessorContext {
187        self.inner as *const AicProcessorContext
188    }
189
190    /// Modifies an enhancement parameter.
191    ///
192    /// All parameters can be changed during audio processing.
193    /// This function can be called from any thread.
194    ///
195    /// This operates on the processor associated with this context handle.
196    ///
197    /// # Arguments
198    ///
199    /// * `parameter` - Parameter to modify
200    /// * `value` - New parameter value. See parameter documentation for ranges
201    ///
202    /// # Returns
203    ///
204    /// Returns `Ok(())` on success or an [`AicError`] if the parameter cannot be set.
205    ///
206    /// # Example
207    ///
208    /// ```rust,no_run
209    /// # use aic_sdk::{Model, ProcessorParameter, Processor};
210    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
211    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
212    /// # let processor = Processor::new(&model, &license_key)?;
213    /// # let proc_ctx = processor.context();
214    /// proc_ctx.set_parameter(ProcessorParameter::EnhancementLevel, 0.8)?;
215    /// # Ok::<(), aic_sdk::AicError>(())
216    /// ```
217    pub fn set_parameter(&self, parameter: ProcessorParameter, value: f32) -> Result<(), AicError> {
218        // SAFETY:
219        // - `self.as_ptr()` is a valid pointer to a live processor context.
220        // - This function can be called from any thread, so we only borrow `&self`.
221        let error_code =
222            unsafe { aic_processor_context_set_parameter(self.as_ptr(), parameter.into(), value) };
223        handle_error(error_code)
224    }
225
226    /// Retrieves the current value of a parameter.
227    ///
228    /// This function can be called from any thread.
229    ///
230    /// This queries the processor associated with this context handle.
231    ///
232    /// # Arguments
233    ///
234    /// * `parameter` - Parameter to query
235    ///
236    /// # Returns
237    ///
238    /// Returns the current parameter value.
239    ///
240    /// # Example
241    ///
242    /// ```rust,no_run
243    /// # use aic_sdk::{Model, ProcessorParameter, Processor};
244    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
245    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
246    /// # let processor = Processor::new(&model, &license_key)?;
247    /// # let processor_context = processor.context();
248    /// let enhancement_level = processor_context.parameter(ProcessorParameter::EnhancementLevel);
249    /// println!("Current enhancement level: {enhancement_level}");
250    /// # Ok::<(), aic_sdk::AicError>(())
251    /// ```
252    pub fn parameter(&self, parameter: ProcessorParameter) -> f32 {
253        let mut value: f32 = 0.0;
254        // SAFETY:
255        // - `self.as_ptr()` is a valid pointer to a live processor context.
256        // - `value` points to stack storage for output.
257        // - This function can be called from any thread, so we only borrow `&self`.
258        let error_code = unsafe {
259            aic_processor_context_get_parameter(self.as_ptr(), parameter.into(), &mut value)
260        };
261        // The wrapper guarantees valid, non-null pointers.
262        assert_success(
263            error_code,
264            "`aic_processor_context_get_parameter` failed. This is a bug, please open an issue on GitHub for further investigation.",
265        );
266        value
267    }
268
269    /// Returns the delay applied to the audio in samples for the current audio configuration.
270    ///
271    /// This function provides the complete end-to-end latency introduced by the processor,
272    /// which includes both algorithmic processing delay and any buffering overhead.
273    /// The processed audio leaves [`Processor::process`] this many samples behind its input.
274    /// Use this value to synchronize enhanced audio with other streams or to implement
275    /// delay compensation in your application.
276    ///
277    /// **Delay behavior:**
278    /// - **Before initialization:** Returns the base processing delay using the model's
279    ///   optimal block size at its native sample rate
280    /// - **After initialization:** Returns the actual delay for your specific configuration,
281    ///   including any additional buffering introduced by a non-optimal block size
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 a block size different from the optimal value returned by
288    /// `optimal_block_size` 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.context();
302    /// let delay = processor_context.audio_delay();
303    /// println!("Audio delay: {} samples", delay);
304    /// # Ok::<(), aic_sdk::AicError>(())
305    /// ```
306    pub fn audio_delay(&self) -> usize {
307        let mut delay: usize = 0;
308        // SAFETY:
309        // - `self.as_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_audio_delay(self.as_ptr(), &mut delay) };
314
315        // This should never fail. If it does, it's a bug in the SDK.
316        // `aic_processor_context_get_audio_delay` is documented to always succeed if given
317        // valid pointers.
318        assert_success(
319            error_code,
320            "`aic_processor_context_get_audio_delay` failed. This is a bug, please open an issue on GitHub for further investigation.",
321        );
322
323        delay
324    }
325
326    /// Clears all internal state and buffers. Any energy VAD is also reset.
327    ///
328    /// Call this when the audio stream is interrupted or when seeking
329    /// to prevent artifacts from previous audio content.
330    ///
331    /// This operates on the processor associated with this context handle.
332    ///
333    /// The processor stays initialized to the configured settings.
334    ///
335    /// # Real-time safety
336    ///
337    /// Real-time safe. Can be called from audio processing threads.
338    ///
339    /// # Example
340    ///
341    /// ```rust,no_run
342    /// # use aic_sdk::{Model, Processor};
343    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
344    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
345    /// # let processor = Processor::new(&model, &license_key)?;
346    /// # let processor_context = processor.context();
347    /// processor_context.reset();
348    /// # Ok::<(), aic_sdk::AicError>(())
349    /// ```
350    pub fn reset(&self) {
351        // SAFETY:
352        // - `self.as_ptr()` is a valid pointer to a live processor context.
353        // - This function can be called from any thread, so we only borrow `&self`.
354        let error_code = unsafe { aic_processor_context_reset(self.as_ptr()) };
355        // The wrapper guarantees valid, non-null pointers.
356        assert_success(
357            error_code,
358            "`aic_processor_context_reset` failed. This is a bug, please open an issue on GitHub for further investigation.",
359        );
360    }
361
362    /// Replaces the bearer token on the running processor.
363    ///
364    /// Use this when your license key is a JWT and needs to be refreshed
365    /// before it expires. Calling this with a renewed token lets you stay authenticated
366    /// without tearing down and recreating the processor: audio processing continues
367    /// uninterrupted, the context handle stays valid, and the new token is used for all
368    /// subsequent authentication against the ai-coustics backend.
369    ///
370    /// In-place updates are only supported when both the originally configured key and the
371    /// new token are JWTs. Other license types cannot be swapped in this way.
372    ///
373    /// On any error the call is a no-op: the previously active token stays in use and the
374    /// telemetry session is unaffected (no backoff, no interruption to processing).
375    ///
376    /// On success the swap is applied immediately and is **not** gated on backend
377    /// acceptance. The token is validated locally for format only; if the backend later
378    /// rejects it (e.g. expired or revoked), the SDK retries it under backoff rather than
379    /// rolling back to the prior token, and audio processing is eventually disabled if no
380    /// accepted token arrives in time. Supplying a known-good token via this call during
381    /// that window recovers the session.
382    ///
383    /// Safe to call concurrently with [`Processor::process`] on the originating processor.
384    ///
385    /// # Arguments
386    ///
387    /// * `token` - The new JWT to install.
388    ///
389    /// # Returns
390    ///
391    /// Returns `Ok(())` on success or an [`AicError`] if the update fails.
392    ///
393    /// # Real-time safety
394    ///
395    /// This function is not real-time safe. It locks a mutex and allocates memory.
396    /// Avoid calling it from audio threads.
397    ///
398    /// # Example
399    ///
400    /// ```rust,no_run
401    /// # use aic_sdk::{Model, Processor};
402    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
403    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
404    /// let processor = Processor::new(&model, &license_key)?;
405    /// let processor_context = processor.context();
406    /// let renewed_jwt = String::from("<JWT_BEARER_TOKEN>");
407    /// processor_context.update_bearer_token(&renewed_jwt)?;
408    /// # Ok::<(), aic_sdk::AicError>(())
409    /// ```
410    pub fn update_bearer_token(&self, token: &str) -> Result<(), AicError> {
411        let c_token = CString::new(token).map_err(|_| AicError::LicenseFormatInvalid)?;
412        // SAFETY:
413        // - `self.as_ptr()` is a valid pointer to a live processor context.
414        // - `c_token` is a null-terminated CString that outlives the call.
415        // - This function can be called from any thread.
416        let error_code =
417            unsafe { aic_processor_context_update_bearer_token(self.as_ptr(), c_token.as_ptr()) };
418        handle_error(error_code)
419    }
420}
421
422impl Drop for ProcessorContext {
423    fn drop(&mut self) {
424        if !self.inner.is_null() {
425            // SAFETY:
426            // - `self.inner` was allocated by the SDK and is still owned by this wrapper.
427            // - This function can be called from any thread; `drop` has exclusive
428            //   access to this context handle.
429            unsafe { aic_processor_context_destroy(self.inner) };
430        }
431    }
432}
433
434// Safety: The underlying C library should be thread-safe for individual ProcessorContext instances
435unsafe impl Send for ProcessorContext {}
436unsafe impl Sync for ProcessorContext {}
437
438/// High-level wrapper for the ai-coustics audio enhancement processor.
439///
440/// A processor is created from an enhancement or bypass model. For voice activity detection,
441/// create a [`Vad`](crate::Vad) from a VAD model instead.
442///
443/// This struct provides a safe, Rust-friendly interface to the underlying C library.
444/// It handles memory management automatically and converts C-style error codes
445/// to Rust `Result` types.
446///
447/// # Example
448///
449/// ```rust,no_run
450/// use aic_sdk::{Model, ProcessorConfig, Processor};
451///
452/// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
453/// let model = Model::from_file("/path/to/model.aicmodel")?;
454/// let config = ProcessorConfig {
455///     block_size: 1024,
456///     ..ProcessorConfig::optimal(&model)
457/// };
458///
459/// let mut processor = Processor::new(&model, &license_key)?.with_config(&config)?;
460///
461/// let mut audio_block = vec![0.0f32; config.block_size];
462/// processor.process(&mut audio_block)?;
463/// # Ok::<(), aic_sdk::AicError>(())
464/// ```
465pub struct Processor<'a> {
466    /// Raw pointer to the C processor structure
467    inner: *mut AicProcessor,
468    /// Whether `initialize` has been called
469    initialized: bool,
470    /// Marker to tie the lifetime of the processor to the lifetime of the model's weights
471    marker: PhantomData<&'a [u8]>,
472}
473
474impl<'a> Processor<'a> {
475    /// Creates a new audio enhancement processor instance.
476    ///
477    /// Multiple processors can be created to process different audio streams simultaneously
478    /// or to switch between different enhancement algorithms during runtime.
479    ///
480    /// The same [`Model`] may be passed to this function more than once: each call creates an
481    /// independent processor that shares the underlying model data internally.
482    ///
483    /// # Arguments
484    ///
485    /// * `model` - The loaded model instance. Must be an enhancement or bypass model,
486    ///   otherwise [`AicError::ModelTypeUnsupported`] is returned.
487    /// * `license_key` - license key for the ai-coustics SDK
488    ///   (generate your key at [developers.ai-coustics.com](https://developers.ai-coustics.com/))
489    ///
490    /// # Returns
491    ///
492    /// Returns a `Result` containing the new `Processor` instance or an [`AicError`] if creation fails.
493    ///
494    /// # Example
495    ///
496    /// ```rust,no_run
497    /// # use aic_sdk::{Model, Processor};
498    /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
499    /// let model = Model::from_file("/path/to/model.aicmodel")?;
500    /// let processor = Processor::new(&model, &license_key)?;
501    /// # Ok::<(), aic_sdk::AicError>(())
502    /// ```
503    pub fn new(model: &Model<'a>, license_key: &str) -> Result<Self, AicError> {
504        Self::create(model, license_key, None)
505    }
506
507    /// Creates a new audio enhancement processor instance with explicit
508    /// OpenTelemetry configuration.
509    ///
510    /// If provided, telemetry will be sent according to the provided configuration. Otherwise
511    /// it will be configured according to the runtime environment.
512    ///
513    /// This overrides the SDK's environment-based telemetry defaults (e.g.
514    /// `AIC_SDK_OTEL_ENABLE`) for this processor.
515    ///
516    /// # Example
517    ///
518    /// ```rust,no_run
519    /// # use aic_sdk::{Model, OtelConfig, Processor};
520    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
521    /// let model = Model::from_file("/path/to/model.aicmodel")?;
522    /// let otel = OtelConfig::enabled();
523    ///
524    /// let processor = Processor::with_otel_config(&model, &license_key, &otel)?;
525    /// # Ok::<(), aic_sdk::AicError>(())
526    /// ```
527    pub fn with_otel_config(
528        model: &Model<'a>,
529        license_key: &str,
530        otel_config: &OtelConfig,
531    ) -> Result<Self, AicError> {
532        Self::create(model, license_key, Some(otel_config))
533    }
534
535    fn create(
536        model: &Model<'a>,
537        license_key: &str,
538        otel_config: Option<&OtelConfig>,
539    ) -> Result<Self, AicError> {
540        // Set the wrapper ID as soon as the user attempts to instantiate a processor.
541        // SAFETY: `2` is the wrapper ID assigned to this Rust SDK.
542        unsafe { crate::set_sdk_id(2) };
543
544        // Session ID must outlive the FFI call so its pointer stays valid.
545        let c_session_id = otel_config
546            .and_then(|o| o.session_id.as_deref())
547            .map(CString::new)
548            .transpose()
549            .map_err(|_| AicError::Internal)?;
550
551        let c_otel = otel_config.map(|o| AicOtelConfig {
552            enable: o.enable,
553            session_id: c_session_id.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
554            export_interval_ms: o.export_interval_ms,
555        });
556        let c_otel_ptr = c_otel
557            .as_ref()
558            .map_or(ptr::null(), |o| o as *const AicOtelConfig);
559
560        let mut processor_ptr: *mut AicProcessor = ptr::null_mut();
561        let c_license_key =
562            CString::new(license_key).map_err(|_| AicError::LicenseFormatInvalid)?;
563
564        // SAFETY:
565        // - `processor_ptr` points to stack storage for output.
566        // - `model` is a valid SDK model pointer for the duration of the call.
567        // - `c_license_key` is a null-terminated CString.
568        // - `c_otel_ptr` is either null or points to a valid `AicOtelConfig` whose
569        //   `session_id` field (if non-null) outlives this call.
570        // - This function is not thread-safe, but the output pointer is local to
571        //   this call and no processor handle exists until it returns.
572        let error_code = unsafe {
573            aic_processor_create(
574                &mut processor_ptr,
575                model.as_ptr(),
576                c_license_key.as_ptr(),
577                c_otel_ptr,
578            )
579        };
580
581        handle_error(error_code)?;
582
583        // This should never happen if the C library is well-behaved, but let's be defensive
584        assert!(
585            !processor_ptr.is_null(),
586            "C library returned success but null pointer"
587        );
588
589        Ok(Self {
590            inner: processor_ptr,
591            initialized: false,
592            marker: PhantomData,
593        })
594    }
595
596    /// Initializes the processor with the given configuration.
597    ///
598    /// This is a convenience method that calls [`Processor::initialize`] internally and returns `self`.
599    /// The processor is immediately ready to process audio after calling this method, so you don't
600    /// need to call [`Processor::initialize`] separately.
601    ///
602    /// # Arguments
603    ///
604    /// * `config` - Audio processing configuration
605    ///
606    /// # Returns
607    ///
608    /// Returns `Ok(Self)` with the initialized processor, or an [`AicError`] if initialization fails.
609    ///
610    /// # Example
611    ///
612    /// ```rust,no_run
613    /// # use aic_sdk::{Model, Processor, ProcessorConfig};
614    /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
615    /// let model = Model::from_file("/path/to/model.aicmodel")?;
616    /// let config = ProcessorConfig::optimal(&model);
617    ///
618    /// let mut processor = Processor::new(&model, &license_key)?.with_config(&config)?;
619    ///
620    /// // Processor is ready to use - no need to call initialize()
621    /// let mut audio_block = vec![0.0f32; config.block_size];
622    /// processor.process(&mut audio_block)?;
623    /// # Ok::<(), aic_sdk::AicError>(())
624    /// ```
625    pub fn with_config(mut self, config: &ProcessorConfig) -> Result<Self, AicError> {
626        self.initialize(config)?;
627        Ok(self)
628    }
629
630    /// Creates a [`ProcessorContext`] instance.
631    /// This can be used to control all parameters and other settings of the processor.
632    ///
633    /// # Example
634    ///
635    /// ```rust,no_run
636    /// # use aic_sdk::{Model, Processor};
637    /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
638    /// let model = Model::from_file("/path/to/model.aicmodel")?;
639    /// let processor = Processor::new(&model, &license_key)?;
640    /// let processor_context = processor.context();
641    /// # Ok::<(), aic_sdk::AicError>(())
642    /// ```
643    pub fn context(&self) -> ProcessorContext {
644        let mut processor_context: *mut AicProcessorContext = ptr::null_mut();
645
646        // SAFETY:
647        // - `processor_context` is valid output storage.
648        // - `self.as_ptr()` is a live processor pointer.
649        // - This function can be called from any thread, so we only borrow `&self`.
650        let error_code =
651            unsafe { aic_processor_context_create(&mut processor_context, self.as_ptr()) };
652
653        // This should never fail
654        assert!(handle_error(error_code).is_ok());
655
656        // This should never happen if the C library is well-behaved, but let's be defensive
657        assert!(
658            !processor_context.is_null(),
659            "C library returned success but null pointer"
660        );
661
662        ProcessorContext::new(processor_context)
663    }
664
665    /// Creates an [`EnergyVadContext`] handle for thread-safe control APIs.
666    ///
667    /// The voice activity detection works automatically as [`Processor::process`] processes audio,
668    /// using the enhanced signal before output mixing.
669    /// The energy VAD shares the processor's enhancement model and does not run a separate model.
670    ///
671    /// This uses the energy VAD associated with this processor.
672    /// All handles created from a given processor reference the same energy VAD instance.
673    ///
674    /// Creating a context keeps enhancement inference active even when the processor is bypassed
675    /// or the enhancement level is zero. This remains active for the processor's lifetime,
676    /// even after all energy VAD context handles are dropped.
677    ///
678    /// **Important:** If the backing processor is dropped, the energy VAD context will stop
679    /// producing new data. It is safe to drop the processor without dropping the context.
680    ///
681    /// # Returns
682    ///
683    /// Returns an [`EnergyVadContext`] associated with this processor.
684    ///
685    /// # Real-time safety
686    ///
687    /// Do not call from audio processing threads as this allocates memory.
688    ///
689    /// # Example
690    ///
691    /// ```rust,no_run
692    /// # use aic_sdk::{Model, Processor};
693    /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
694    /// let model = Model::from_file("/path/to/enhancement_model.aicmodel")?;
695    /// let mut processor = Processor::new(&model, &license_key)?;
696    /// let vad_ctx = processor.energy_vad_context();
697    /// # Ok::<(), aic_sdk::AicError>(())
698    /// ```
699    pub fn energy_vad_context(&mut self) -> EnergyVadContext {
700        let mut context_ptr: *mut AicEnergyVadContext = ptr::null_mut();
701
702        // SAFETY:
703        // - `self.as_ptr()` points to a live processor.
704        // - `context_ptr` is valid output storage and not aliased.
705        // - `&mut self` prevents concurrent use or destruction of the processor.
706        let error_code = unsafe { aic_energy_vad_context_create(&mut context_ptr, self.as_ptr()) };
707
708        // This should never fail
709        assert!(handle_error(error_code).is_ok());
710
711        // This should never happen if the C library is well-behaved, but let's be defensive
712        assert!(
713            !context_ptr.is_null(),
714            "C library returned success but null pointer"
715        );
716
717        EnergyVadContext::new(context_ptr)
718    }
719
720    /// Configures the processor for specific audio settings.
721    ///
722    /// This function must be called before processing any audio.
723    /// For the lowest delay use the sample rate and block size returned by
724    /// [`Model::optimal_sample_rate`] and [`Model::optimal_block_size`].
725    ///
726    /// # Arguments
727    ///
728    /// * `config` - Audio processing configuration
729    ///
730    /// # Returns
731    ///
732    /// Returns `Ok(())` on success or an [`AicError`] if initialization fails.
733    ///
734    /// # Warning
735    /// Do not call from audio processing threads as this allocates memory.
736    ///
737    /// # Example
738    ///
739    /// ```rust,no_run
740    /// # use aic_sdk::{Model, Processor, ProcessorConfig};
741    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
742    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
743    /// # let mut processor = Processor::new(&model, &license_key)?;
744    /// let config = ProcessorConfig::optimal(&model);
745    /// processor.initialize(&config)?;
746    /// # Ok::<(), aic_sdk::AicError>(())
747    /// ```
748    pub fn initialize(&mut self, config: &ProcessorConfig) -> Result<(), AicError> {
749        // SAFETY:
750        // - `self.inner` is a valid pointer to a live processor.
751        // - This function is not thread-safe, so we borrow `&mut self`.
752        let error_code = unsafe {
753            aic_processor_initialize(
754                self.inner,
755                config.sample_rate,
756                config.block_size,
757                config.variable_block_size,
758            )
759        };
760
761        handle_error(error_code)?;
762        self.initialized = true;
763        Ok(())
764    }
765
766    /// Processes mono audio.
767    ///
768    /// Enhances speech in the provided audio block in-place.
769    ///
770    /// # Arguments
771    ///
772    /// * `audio` - Mono audio block to be enhanced in-place. Must match `block_size` from
773    ///   initialization, or if `variable_block_size` was enabled, must be less than or equal
774    ///   to `block_size`.
775    ///
776    /// # Returns
777    ///
778    /// Returns `Ok(())` on success or an [`AicError`] if processing fails.
779    ///
780    /// # Real-time safety
781    ///
782    /// Real-time safe. Can be called from audio processing threads.
783    ///
784    /// # Example
785    ///
786    /// ```rust,no_run
787    /// # use aic_sdk::{Model, Processor, ProcessorConfig};
788    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
789    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
790    /// # let mut processor = Processor::new(&model, &license_key)?;
791    /// let config = ProcessorConfig::optimal(&model);
792    /// processor.initialize(&config)?;
793    /// let mut audio = vec![0.0f32; config.block_size];
794    /// processor.process(&mut audio)?;
795    /// # Ok::<(), aic_sdk::AicError>(())
796    /// ```
797    pub fn process(&mut self, audio: &mut [f32]) -> Result<(), AicError> {
798        if !self.initialized {
799            return Err(AicError::NotInitialized);
800        }
801
802        let audio_len = audio.len();
803
804        // SAFETY:
805        // - `self.inner` is a valid pointer to a live processor.
806        // - `audio` points to a contiguous, writable f32 slice of length `audio_len`.
807        // - This function is not thread-safe, so we borrow `&mut self`.
808        let error_code =
809            unsafe { aic_processor_process(self.inner, audio.as_mut_ptr(), audio_len) };
810
811        handle_error(error_code)
812    }
813
814    /// Terminates the telemetry session associated with this processor.
815    ///
816    /// Once the request has been handled, the processor is no longer allowed to process audio.
817    ///
818    /// This function is meant to be used in lifecycle management events.
819    /// A telemetry session is automatically stopped when a processor is destroyed.
820    /// However, in cases where this SDK is integrated with languages with automatic memory
821    /// management, object deallocation could be delayed. Use this function to terminate
822    /// the session explicitly.
823    ///
824    /// This function blocks until the telemetry session is terminated, unless another
825    /// session is still alive. In that case, this function returns early and termination
826    /// happens asynchronously. This keeps lifecycle management smooth while ensuring
827    /// all sessions are closed when the last processor is terminated.
828    ///
829    /// # Real-time safety
830    ///
831    /// This function is not real-time safe. It may block until the session is terminated.
832    /// Avoid calling it from audio threads.
833    ///
834    /// # Example
835    ///
836    /// ```rust,no_run
837    /// # use aic_sdk::{Model, Processor};
838    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
839    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
840    /// let mut processor = Processor::new(&model, &license_key)?;
841    /// processor.terminate_session();
842    /// # Ok::<(), aic_sdk::AicError>(())
843    /// ```
844    pub fn terminate_session(&mut self) {
845        // SAFETY:
846        // - `self.inner` is a valid pointer to a live processor.
847        // - This function must not run concurrently with any other call taking the same
848        //   processor handle, so we borrow `&mut self`.
849        let error_code = unsafe { aic_processor_terminate_session(self.inner) };
850        // The wrapper guarantees valid, non-null pointers.
851        assert_success(
852            error_code,
853            "`aic_processor_terminate_session` failed. This is a bug, please open an issue on GitHub for further investigation.",
854        );
855    }
856
857    fn as_ptr(&self) -> *const AicProcessor {
858        self.inner as *const AicProcessor
859    }
860}
861
862impl<'a> Drop for Processor<'a> {
863    fn drop(&mut self) {
864        if !self.inner.is_null() {
865            // SAFETY:
866            // - `self.inner` was allocated by the SDK and is still owned by this wrapper.
867            // - This function is not thread-safe with concurrent processor use, but
868            //   `drop` has exclusive access to `self`.
869            unsafe { aic_processor_destroy(self.inner) };
870        }
871    }
872}
873
874// SAFETY: Everything in Processor is Send, with the exception of the inner raw pointer.
875// The Processor only uses the raw pointer according to the safety contracts of the
876// unsafe APIs that require the pointer, and the Processor does not expose access to the
877// raw pointer in any of its methods. Therefore, it safe to implement Send for Processor.
878unsafe impl<'a> Send for Processor<'a> {}
879
880// SAFETY: Processor does not expose any interior mutability. The SDK functions that are documented
881// as not thread-safe (`aic_processor_initialize`, `aic_processor_process`,
882// `aic_processor_terminate_session`, `aic_processor_destroy`) are only reachable through methods
883// that take `&mut self` or through `drop`, so Rust's borrow rules serialize them. The only method
884// that takes `&self` (`context`) just creates a new context handle from a const
885// processor pointer, which is safe to do while the processor is in use on another thread.
886// Therefore, it is safe to implement Sync for Processor.
887unsafe impl<'a> Sync for Processor<'a> {}
888
889#[cfg(test)]
890mod tests {
891    use super::*;
892    use crate::test_support::{license_key, test_model_path};
893
894    const TEST_MODEL_ID: &str = "rook-s-48khz";
895
896    fn load_test_model() -> Result<(Model<'static>, String), AicError> {
897        let model = Model::from_file(test_model_path(TEST_MODEL_ID))?;
898
899        Ok((model, license_key()))
900    }
901
902    #[test]
903    fn model_creation_and_basic_operations() {
904        dbg!(crate::get_sdk_version());
905        dbg!(crate::get_compatible_model_version());
906
907        let (model, license_key) = load_test_model().unwrap();
908        let config = ProcessorConfig::optimal(&model);
909
910        let mut processor = Processor::new(&model, &license_key)
911            .unwrap()
912            .with_config(&config)
913            .unwrap();
914
915        let mut audio = vec![0.0f32; config.block_size];
916        processor.process(&mut audio).unwrap();
917    }
918
919    #[test]
920    fn process_fixed_block_size() {
921        let (model, license_key) = load_test_model().unwrap();
922        let config = ProcessorConfig::optimal(&model);
923
924        let mut processor = Processor::new(&model, &license_key)
925            .unwrap()
926            .with_config(&config)
927            .unwrap();
928
929        let mut audio = vec![0.0f32; config.block_size];
930        processor.process(&mut audio).unwrap();
931    }
932
933    #[test]
934    fn process_variable_block_size() {
935        let (model, license_key) = load_test_model().unwrap();
936        let config = ProcessorConfig::optimal(&model).with_variable_block_size(true);
937
938        let mut processor = Processor::new(&model, &license_key)
939            .unwrap()
940            .with_config(&config)
941            .unwrap();
942
943        let mut audio = vec![0.0f32; config.block_size];
944        processor.process(&mut audio).unwrap();
945
946        let mut audio = vec![0.0f32; 20];
947        processor.process(&mut audio).unwrap();
948    }
949
950    #[test]
951    fn process_variable_block_size_fails_when_disabled() {
952        let (model, license_key) = load_test_model().unwrap();
953        let config = ProcessorConfig::optimal(&model);
954
955        let mut processor = Processor::new(&model, &license_key)
956            .unwrap()
957            .with_config(&config)
958            .unwrap();
959
960        let mut audio = vec![0.0f32; config.block_size];
961        processor.process(&mut audio).unwrap();
962
963        let mut audio = vec![0.0f32; 20];
964        let result = processor.process(&mut audio);
965        assert_eq!(result, Err(AicError::AudioConfigMismatch));
966    }
967
968    #[test]
969    fn model_can_be_dropped_after_creating_processor() {
970        let (model, license_key) = load_test_model().unwrap();
971        let config = ProcessorConfig::optimal(&model);
972
973        let mut processor = Processor::new(&model, &license_key)
974            .unwrap()
975            .with_config(&config)
976            .unwrap();
977        drop(model); // Inside of the SDK an Arc-Pointer to `Model` is stored in Processor, so it won't be de-allocated
978
979        let mut audio = vec![0.0f32; config.block_size];
980        processor.process(&mut audio).unwrap();
981    }
982
983    #[test]
984    fn processor_is_send_and_sync() {
985        // Compile-time check that Processor implements Send and Sync.
986        // This ensures the processor can be safely moved to another thread.
987        fn assert_send<T: Send>() {}
988        fn assert_sync<T: Send>() {}
989
990        assert_send::<Processor>();
991        assert_sync::<Processor>();
992    }
993
994    struct MyModel {
995        _model: Model<'static>,
996        _processor: Processor<'static>,
997    }
998
999    impl MyModel {
1000        pub fn new() -> Self {
1001            let (model, license_key) = load_test_model().unwrap();
1002            let processor = Processor::new(&model, &license_key)
1003                .unwrap()
1004                .with_config(&ProcessorConfig::optimal(&model))
1005                .unwrap();
1006            MyModel {
1007                _model: model,
1008                _processor: processor,
1009            }
1010        }
1011    }
1012
1013    #[test]
1014    fn can_create_self_referential_structs_with_statics() {
1015        let _model = MyModel::new();
1016    }
1017}
1018
1019#[doc(hidden)]
1020mod _compile_fail_tests {
1021    //! Compile-fail regression: a `Processor`'s model buffer must not be dropped before the processor.
1022    //!
1023    //! ```rust,compile_fail
1024    //! use aic_sdk::{Model, Processor, ProcessorConfig};
1025    //!
1026    //! fn main() {
1027    //!     let buffer = vec![0u8; 64];
1028    //!     let model = Model::from_buffer(&buffer).unwrap();
1029    //!     let config = ProcessorConfig::optimal(&model);
1030    //!
1031    //!     let mut processor = Processor::new(&model, "license")
1032    //!         .unwrap()
1033    //!         .with_config(&config)
1034    //!         .unwrap();
1035    //!
1036    //!     drop(model); // Model can be dropped without issues
1037    //!
1038    //!     drop(buffer); // This should fail to compile
1039    //!
1040    //!     let mut audio = vec![0.0f32; config.block_size];
1041    //!     processor.process(&mut audio).unwrap();
1042    //! }
1043    //! ```
1044}