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/// [`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_const_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_const_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 = unsafe {
222            aic_processor_context_set_parameter(self.as_const_ptr(), parameter.into(), value)
223        };
224        handle_error(error_code)
225    }
226
227    /// Retrieves the current value of a parameter.
228    ///
229    /// This function can be called from any thread.
230    ///
231    /// This queries the processor associated with this context handle.
232    ///
233    /// # Arguments
234    ///
235    /// * `parameter` - Parameter to query
236    ///
237    /// # Returns
238    ///
239    /// Returns `Ok(value)` containing the current parameter value, or an [`AicError`] if the query fails.
240    ///
241    /// # Example
242    ///
243    /// ```rust,no_run
244    /// # use aic_sdk::{Model, ProcessorParameter, Processor};
245    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
246    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
247    /// # let processor = Processor::new(&model, &license_key)?;
248    /// # let processor_context = processor.context();
249    /// let enhancement_level = processor_context.parameter(ProcessorParameter::EnhancementLevel)?;
250    /// println!("Current enhancement level: {enhancement_level}");
251    /// # Ok::<(), aic_sdk::AicError>(())
252    /// ```
253    pub fn parameter(&self, parameter: ProcessorParameter) -> Result<f32, AicError> {
254        let mut value: f32 = 0.0;
255        // SAFETY:
256        // - `self.as_const_ptr()` is a valid pointer to a live processor context.
257        // - `value` points to stack storage for output.
258        // - This function can be called from any thread, so we only borrow `&self`.
259        let error_code = unsafe {
260            aic_processor_context_get_parameter(self.as_const_ptr(), parameter.into(), &mut value)
261        };
262        handle_error(error_code)?;
263        Ok(value)
264    }
265
266    /// Returns the delay applied to the audio in samples for the current audio configuration.
267    ///
268    /// This function provides the complete end-to-end latency introduced by the processor,
269    /// which includes both algorithmic processing delay and any buffering overhead.
270    /// The processed audio leaves [`Processor::process`] this many samples behind its input.
271    /// Use this value to synchronize enhanced audio with other streams or to implement
272    /// delay compensation in your application.
273    ///
274    /// **Delay behavior:**
275    /// - **Before initialization:** Returns the base processing delay using the model's
276    ///   optimal block size at its native sample rate
277    /// - **After initialization:** Returns the actual delay for your specific configuration,
278    ///   including any additional buffering introduced by a non-optimal block size
279    ///
280    /// **Important:** The delay value is always expressed in samples at the sample rate
281    /// you configured during `initialize`. To convert to time units:
282    /// `delay_ms = (delay_samples * 1000) / sample_rate`
283    ///
284    /// **Note:** Using a block size different from the optimal value returned by
285    /// `optimal_block_size` will increase the delay beyond the model's base latency.
286    ///
287    /// # Returns
288    ///
289    /// Returns the delay in samples.
290    ///
291    /// # Example
292    ///
293    /// ```rust,no_run
294    /// # use aic_sdk::{Model, Processor};
295    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
296    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
297    /// # let processor = Processor::new(&model, &license_key)?;
298    /// # let processor_context = processor.context();
299    /// let delay = processor_context.audio_delay();
300    /// println!("Audio delay: {} samples", delay);
301    /// # Ok::<(), aic_sdk::AicError>(())
302    /// ```
303    pub fn audio_delay(&self) -> usize {
304        let mut delay: usize = 0;
305        // SAFETY:
306        // - `self.as_const_ptr()` is a valid pointer to a live processor context.
307        // - `delay` points to stack storage for output.
308        // - This function can be called from any thread, so we only borrow `&self`.
309        let error_code =
310            unsafe { aic_processor_context_get_audio_delay(self.as_const_ptr(), &mut delay) };
311
312        // This should never fail. If it does, it's a bug in the SDK.
313        // `aic_processor_context_get_audio_delay` is documented to always succeed if given
314        // valid pointers.
315        assert_success(
316            error_code,
317            "`aic_processor_context_get_audio_delay` failed. This is a bug, please open an issue on GitHub for further investigation.",
318        );
319
320        delay
321    }
322
323    /// Clears all internal state and buffers.
324    ///
325    /// Call this when the audio stream is interrupted or when seeking
326    /// to prevent artifacts from previous audio content.
327    ///
328    /// This operates on the processor associated with this context handle.
329    ///
330    /// The processor stays initialized to the configured settings.
331    ///
332    /// # Returns
333    ///
334    /// Returns `Ok(())` on success or an [`AicError`] if the reset fails.
335    ///
336    /// # Real-time safety
337    ///
338    /// Real-time safe. Can be called from audio processing threads.
339    ///
340    /// # Example
341    ///
342    /// ```rust,no_run
343    /// # use aic_sdk::{Model, Processor};
344    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
345    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
346    /// # let processor = Processor::new(&model, &license_key)?;
347    /// # let processor_context = processor.context();
348    /// processor_context.reset()?;
349    /// # Ok::<(), aic_sdk::AicError>(())
350    /// ```
351    pub fn reset(&self) -> Result<(), AicError> {
352        // SAFETY:
353        // - `self.as_const_ptr()` is a valid pointer to a live processor context.
354        // - This function can be called from any thread, so we only borrow `&self`.
355        let error_code = unsafe { aic_processor_context_reset(self.as_const_ptr()) };
356        handle_error(error_code)
357    }
358
359    /// Replaces the bearer token on the running processor.
360    ///
361    /// Use this when your license key is a JWT and needs to be refreshed
362    /// before it expires. Calling this with a renewed token lets you stay authenticated
363    /// without tearing down and recreating the processor: audio processing continues
364    /// uninterrupted, the context handle stays valid, and the new token is used for all
365    /// subsequent authentication against the ai-coustics backend.
366    ///
367    /// In-place updates are only supported when both the originally configured key and the
368    /// new token are JWTs. Other license types cannot be swapped in this way.
369    ///
370    /// On any error the call is a no-op: the previously active token stays in use and the
371    /// telemetry session is unaffected (no backoff, no interruption to processing).
372    ///
373    /// On success the swap is applied immediately and is **not** gated on backend
374    /// acceptance. The token is validated locally for format only; if the backend later
375    /// rejects it (e.g. expired or revoked), the SDK retries it under backoff rather than
376    /// rolling back to the prior token, and audio processing is eventually disabled if no
377    /// accepted token arrives in time. Supplying a known-good token via this call during
378    /// that window recovers the session.
379    ///
380    /// Safe to call concurrently with [`Processor::process`] on the originating processor.
381    ///
382    /// # Arguments
383    ///
384    /// * `token` - The new JWT to install.
385    ///
386    /// # Returns
387    ///
388    /// Returns `Ok(())` on success or an [`AicError`] if the update fails.
389    ///
390    /// # Real-time safety
391    ///
392    /// This function is not real-time safe. It locks a mutex and allocates memory.
393    /// Avoid calling it from audio threads.
394    ///
395    /// # Example
396    ///
397    /// ```rust,no_run
398    /// # use aic_sdk::{Model, Processor};
399    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
400    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
401    /// let processor = Processor::new(&model, &license_key)?;
402    /// let processor_context = processor.context();
403    /// let renewed_jwt = String::from("<JWT_BEARER_TOKEN>");
404    /// processor_context.update_bearer_token(&renewed_jwt)?;
405    /// # Ok::<(), aic_sdk::AicError>(())
406    /// ```
407    pub fn update_bearer_token(&self, token: &str) -> Result<(), AicError> {
408        let c_token = CString::new(token).map_err(|_| AicError::LicenseFormatInvalid)?;
409        // SAFETY:
410        // - `self.as_const_ptr()` is a valid pointer to a live processor context.
411        // - `c_token` is a null-terminated CString that outlives the call.
412        // - This function can be called from any thread.
413        let error_code = unsafe {
414            aic_processor_context_update_bearer_token(self.as_const_ptr(), c_token.as_ptr())
415        };
416        handle_error(error_code)
417    }
418}
419
420impl Drop for ProcessorContext {
421    fn drop(&mut self) {
422        if !self.inner.is_null() {
423            // SAFETY:
424            // - `self.inner` was allocated by the SDK and is still owned by this wrapper.
425            // - This function can be called from any thread; `drop` has exclusive
426            //   access to this context handle.
427            unsafe { aic_processor_context_destroy(self.inner) };
428        }
429    }
430}
431
432// Safety: The underlying C library should be thread-safe for individual ProcessorContext instances
433unsafe impl Send for ProcessorContext {}
434unsafe impl Sync for ProcessorContext {}
435
436/// High-level wrapper for the ai-coustics audio enhancement processor.
437///
438/// A processor is created from an enhancement or bypass model. For voice activity detection,
439/// create a [`Vad`](crate::Vad) from a VAD model instead.
440///
441/// This struct provides a safe, Rust-friendly interface to the underlying C library.
442/// It handles memory management automatically and converts C-style error codes
443/// to Rust `Result` types.
444///
445/// # Example
446///
447/// ```rust,no_run
448/// use aic_sdk::{Model, ProcessorConfig, Processor};
449///
450/// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
451/// let model = Model::from_file("/path/to/model.aicmodel")?;
452/// let config = ProcessorConfig {
453///     block_size: 1024,
454///     ..ProcessorConfig::optimal(&model)
455/// };
456///
457/// let mut processor = Processor::new(&model, &license_key)?.with_config(&config)?;
458///
459/// let mut audio_block = vec![0.0f32; config.block_size];
460/// processor.process(&mut audio_block)?;
461/// # Ok::<(), aic_sdk::AicError>(())
462/// ```
463pub struct Processor<'a> {
464    /// Raw pointer to the C processor structure
465    inner: *mut AicProcessor,
466    /// Whether `initialize` has been called
467    initialized: bool,
468    /// Marker to tie the lifetime of the processor to the lifetime of the model's weights
469    marker: PhantomData<&'a [u8]>,
470}
471
472impl<'a> Processor<'a> {
473    /// Creates a new audio enhancement processor instance.
474    ///
475    /// Multiple processors can be created to process different audio streams simultaneously
476    /// or to switch between different enhancement algorithms during runtime.
477    ///
478    /// The same [`Model`] may be passed to this function more than once: each call creates an
479    /// independent processor that shares the underlying model data internally.
480    ///
481    /// # Arguments
482    ///
483    /// * `model` - The loaded model instance. Must be an enhancement or bypass model,
484    ///   otherwise [`AicError::ModelTypeUnsupported`] is returned.
485    /// * `license_key` - license key for the ai-coustics SDK
486    ///   (generate your key at [developers.ai-coustics.com](https://developers.ai-coustics.com/))
487    ///
488    /// # Returns
489    ///
490    /// Returns a `Result` containing the new `Processor` instance or an [`AicError`] if creation fails.
491    ///
492    /// # Example
493    ///
494    /// ```rust,no_run
495    /// # use aic_sdk::{Model, Processor};
496    /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
497    /// let model = Model::from_file("/path/to/model.aicmodel")?;
498    /// let processor = Processor::new(&model, &license_key)?;
499    /// # Ok::<(), aic_sdk::AicError>(())
500    /// ```
501    pub fn new(model: &Model<'a>, license_key: &str) -> Result<Self, AicError> {
502        Self::create(model, license_key, None)
503    }
504
505    /// Creates a new audio enhancement processor instance with explicit
506    /// OpenTelemetry configuration.
507    ///
508    /// If provided, telemetry will be sent according to the provided configuration. Otherwise
509    /// it will be configured according to the runtime environment.
510    ///
511    /// This overrides the SDK's environment-based telemetry defaults (e.g.
512    /// `AIC_SDK_OTEL_ENABLE`) for this processor.
513    ///
514    /// # Example
515    ///
516    /// ```rust,no_run
517    /// # use aic_sdk::{Model, OtelConfig, Processor};
518    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
519    /// let model = Model::from_file("/path/to/model.aicmodel")?;
520    /// let otel = OtelConfig::enabled();
521    ///
522    /// let processor = Processor::with_otel_config(&model, &license_key, &otel)?;
523    /// # Ok::<(), aic_sdk::AicError>(())
524    /// ```
525    pub fn with_otel_config(
526        model: &Model<'a>,
527        license_key: &str,
528        otel_config: &OtelConfig,
529    ) -> Result<Self, AicError> {
530        Self::create(model, license_key, Some(otel_config))
531    }
532
533    fn create(
534        model: &Model<'a>,
535        license_key: &str,
536        otel_config: Option<&OtelConfig>,
537    ) -> Result<Self, AicError> {
538        // Set the wrapper ID as soon as the user attempts to instantiate a processor.
539        // SAFETY: `2` is the wrapper ID assigned to this Rust SDK.
540        unsafe { crate::set_sdk_id(2) };
541
542        // Session ID must outlive the FFI call so its pointer stays valid.
543        let c_session_id = otel_config
544            .and_then(|o| o.session_id.as_deref())
545            .map(CString::new)
546            .transpose()
547            .map_err(|_| AicError::Internal)?;
548
549        let c_otel = otel_config.map(|o| AicOtelConfig {
550            enable: o.enable,
551            session_id: c_session_id.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
552            export_interval_ms: o.export_interval_ms,
553        });
554        let c_otel_ptr = c_otel
555            .as_ref()
556            .map_or(ptr::null(), |o| o as *const AicOtelConfig);
557
558        let mut processor_ptr: *mut AicProcessor = ptr::null_mut();
559        let c_license_key =
560            CString::new(license_key).map_err(|_| AicError::LicenseFormatInvalid)?;
561
562        // SAFETY:
563        // - `processor_ptr` points to stack storage for output.
564        // - `model` is a valid SDK model pointer for the duration of the call.
565        // - `c_license_key` is a null-terminated CString.
566        // - `c_otel_ptr` is either null or points to a valid `AicOtelConfig` whose
567        //   `session_id` field (if non-null) outlives this call.
568        // - This function is not thread-safe, but the output pointer is local to
569        //   this call and no processor handle exists until it returns.
570        let error_code = unsafe {
571            aic_processor_create(
572                &mut processor_ptr,
573                model.as_const_ptr(),
574                c_license_key.as_ptr(),
575                c_otel_ptr,
576            )
577        };
578
579        handle_error(error_code)?;
580
581        // This should never happen if the C library is well-behaved, but let's be defensive
582        assert!(
583            !processor_ptr.is_null(),
584            "C library returned success but null pointer"
585        );
586
587        Ok(Self {
588            inner: processor_ptr,
589            initialized: false,
590            marker: PhantomData,
591        })
592    }
593
594    /// Initializes the processor with the given configuration.
595    ///
596    /// This is a convenience method that calls [`Processor::initialize`] internally and returns `self`.
597    /// The processor is immediately ready to process audio after calling this method, so you don't
598    /// need to call [`Processor::initialize`] separately.
599    ///
600    /// # Arguments
601    ///
602    /// * `config` - Audio processing configuration
603    ///
604    /// # Returns
605    ///
606    /// Returns `Ok(Self)` with the initialized processor, or an [`AicError`] if initialization fails.
607    ///
608    /// # Example
609    ///
610    /// ```rust,no_run
611    /// # use aic_sdk::{Model, Processor, ProcessorConfig};
612    /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
613    /// let model = Model::from_file("/path/to/model.aicmodel")?;
614    /// let config = ProcessorConfig::optimal(&model);
615    ///
616    /// let mut processor = Processor::new(&model, &license_key)?.with_config(&config)?;
617    ///
618    /// // Processor is ready to use - no need to call initialize()
619    /// let mut audio_block = vec![0.0f32; config.block_size];
620    /// processor.process(&mut audio_block)?;
621    /// # Ok::<(), aic_sdk::AicError>(())
622    /// ```
623    pub fn with_config(mut self, config: &ProcessorConfig) -> Result<Self, AicError> {
624        self.initialize(config)?;
625        Ok(self)
626    }
627
628    /// Creates a [ProcessorContext] instance.
629    /// This can be used to control all parameters and other settings of the processor.
630    ///
631    /// # Example
632    ///
633    /// ```rust,no_run
634    /// # use aic_sdk::{Model, Processor};
635    /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
636    /// let model = Model::from_file("/path/to/model.aicmodel")?;
637    /// let processor = Processor::new(&model, &license_key)?;
638    /// let processor_context = processor.context();
639    /// # Ok::<(), aic_sdk::AicError>(())
640    /// ```
641    pub fn context(&self) -> ProcessorContext {
642        let mut processor_context: *mut AicProcessorContext = ptr::null_mut();
643
644        // SAFETY:
645        // - `processor_context` is valid output storage.
646        // - `self.as_const_ptr()` is a live processor pointer.
647        // - This function can be called from any thread, so we only borrow `&self`.
648        let error_code =
649            unsafe { aic_processor_context_create(&mut processor_context, self.as_const_ptr()) };
650
651        // This should never fail
652        assert!(handle_error(error_code).is_ok());
653
654        // This should never happen if the C library is well-behaved, but let's be defensive
655        assert!(
656            !processor_context.is_null(),
657            "C library returned success but null pointer"
658        );
659
660        ProcessorContext::new(processor_context)
661    }
662
663    /// Configures the processor for specific audio settings.
664    ///
665    /// This function must be called before processing any audio.
666    /// For the lowest delay use the sample rate and block size returned by
667    /// [`Model::optimal_sample_rate`] and [`Model::optimal_block_size`].
668    ///
669    /// # Arguments
670    ///
671    /// * `config` - Audio processing configuration
672    ///
673    /// # Returns
674    ///
675    /// Returns `Ok(())` on success or an [`AicError`] if initialization fails.
676    ///
677    /// # Warning
678    /// Do not call from audio processing threads as this allocates memory.
679    ///
680    /// # Example
681    ///
682    /// ```rust,no_run
683    /// # use aic_sdk::{Model, Processor, ProcessorConfig};
684    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
685    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
686    /// # let mut processor = Processor::new(&model, &license_key)?;
687    /// let config = ProcessorConfig::optimal(&model);
688    /// processor.initialize(&config)?;
689    /// # Ok::<(), aic_sdk::AicError>(())
690    /// ```
691    pub fn initialize(&mut self, config: &ProcessorConfig) -> Result<(), AicError> {
692        // SAFETY:
693        // - `self.inner` is a valid pointer to a live processor.
694        // - This function is not thread-safe, so we borrow `&mut self`.
695        let error_code = unsafe {
696            aic_processor_initialize(
697                self.inner,
698                config.sample_rate,
699                config.block_size,
700                config.variable_block_size,
701            )
702        };
703
704        handle_error(error_code)?;
705        self.initialized = true;
706        Ok(())
707    }
708
709    /// Processes mono audio.
710    ///
711    /// Enhances speech in the provided audio block in-place.
712    ///
713    /// # Arguments
714    ///
715    /// * `audio` - Mono audio block to be enhanced in-place. Must match `block_size` from
716    ///   initialization, or if `variable_block_size` was enabled, must be less than or equal
717    ///   to `block_size`.
718    ///
719    /// # Returns
720    ///
721    /// Returns `Ok(())` on success or an [`AicError`] if processing fails.
722    ///
723    /// # Real-time safety
724    ///
725    /// Real-time safe. Can be called from audio processing threads.
726    ///
727    /// # Example
728    ///
729    /// ```rust,no_run
730    /// # use aic_sdk::{Model, Processor, ProcessorConfig};
731    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
732    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
733    /// # let mut processor = Processor::new(&model, &license_key)?;
734    /// let config = ProcessorConfig::optimal(&model);
735    /// processor.initialize(&config)?;
736    /// let mut audio = vec![0.0f32; config.block_size];
737    /// processor.process(&mut audio)?;
738    /// # Ok::<(), aic_sdk::AicError>(())
739    /// ```
740    pub fn process(&mut self, audio: &mut [f32]) -> Result<(), AicError> {
741        if !self.initialized {
742            return Err(AicError::NotInitialized);
743        }
744
745        let audio_len = audio.len();
746
747        // SAFETY:
748        // - `self.inner` is a valid pointer to a live processor.
749        // - `audio` points to a contiguous, writable f32 slice of length `audio_len`.
750        // - This function is not thread-safe, so we borrow `&mut self`.
751        let error_code =
752            unsafe { aic_processor_process(self.inner, audio.as_mut_ptr(), audio_len) };
753
754        handle_error(error_code)
755    }
756
757    /// Terminates the telemetry session associated with this processor.
758    ///
759    /// Once the request has been handled, the processor is no longer allowed to process audio.
760    ///
761    /// This function is meant to be used in lifecycle management events.
762    /// A telemetry session is automatically stopped when a processor is destroyed.
763    /// However, in cases where this SDK is integrated with languages with automatic memory
764    /// management, object deallocation could be delayed. Use this function to terminate
765    /// the session explicitly.
766    ///
767    /// This function blocks until the telemetry session is terminated, unless another
768    /// session is still alive. In that case, this function returns early and termination
769    /// happens asynchronously. This keeps lifecycle management smooth while ensuring
770    /// all sessions are closed when the last processor is terminated.
771    ///
772    /// # Returns
773    ///
774    /// Returns `Ok(())` on success or an [`AicError`] if termination cannot be requested.
775    ///
776    /// # Real-time safety
777    ///
778    /// This function is not real-time safe. It may block until the session is terminated.
779    /// Avoid calling it from audio threads.
780    ///
781    /// # Example
782    ///
783    /// ```rust,no_run
784    /// # use aic_sdk::{Model, Processor};
785    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
786    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
787    /// let mut processor = Processor::new(&model, &license_key)?;
788    /// processor.terminate_session()?;
789    /// # Ok::<(), aic_sdk::AicError>(())
790    /// ```
791    pub fn terminate_session(&mut self) -> Result<(), AicError> {
792        // SAFETY:
793        // - `self.inner` is a valid pointer to a live processor.
794        // - This function must not run concurrently with any other call taking the same
795        //   processor handle, so we borrow `&mut self`.
796        let error_code = unsafe { aic_processor_terminate_session(self.inner) };
797        handle_error(error_code)
798    }
799
800    fn as_const_ptr(&self) -> *const AicProcessor {
801        self.inner as *const AicProcessor
802    }
803}
804
805impl<'a> Drop for Processor<'a> {
806    fn drop(&mut self) {
807        if !self.inner.is_null() {
808            // SAFETY:
809            // - `self.inner` was allocated by the SDK and is still owned by this wrapper.
810            // - This function is not thread-safe with concurrent processor use, but
811            //   `drop` has exclusive access to `self`.
812            unsafe { aic_processor_destroy(self.inner) };
813        }
814    }
815}
816
817// SAFETY: Everything in Processor is Send, with the exception of the inner raw pointer.
818// The Processor only uses the raw pointer according to the safety contracts of the
819// unsafe APIs that require the pointer, and the Processor does not expose access to the
820// raw pointer in any of its methods. Therefore, it safe to implement Send for Processor.
821unsafe impl<'a> Send for Processor<'a> {}
822
823// SAFETY: Processor does not expose any interior mutability. The SDK functions that are documented
824// as not thread-safe (`aic_processor_initialize`, `aic_processor_process`,
825// `aic_processor_terminate_session`, `aic_processor_destroy`) are only reachable through methods
826// that take `&mut self` or through `drop`, so Rust's borrow rules serialize them. The only method
827// that takes `&self` (`context`) just creates a new context handle from a const
828// processor pointer, which is safe to do while the processor is in use on another thread.
829// Therefore, it is safe to implement Sync for Processor.
830unsafe impl<'a> Sync for Processor<'a> {}
831
832#[cfg(test)]
833mod tests {
834    use super::*;
835    use crate::test_support::{license_key, test_model_path};
836
837    const TEST_MODEL_ID: &str = "rook-s-48khz";
838
839    fn load_test_model() -> Result<(Model<'static>, String), AicError> {
840        let model = Model::from_file(test_model_path(TEST_MODEL_ID))?;
841
842        Ok((model, license_key()))
843    }
844
845    #[test]
846    fn model_creation_and_basic_operations() {
847        dbg!(crate::get_sdk_version());
848        dbg!(crate::get_compatible_model_version());
849
850        let (model, license_key) = load_test_model().unwrap();
851        let config = ProcessorConfig::optimal(&model);
852
853        let mut processor = Processor::new(&model, &license_key)
854            .unwrap()
855            .with_config(&config)
856            .unwrap();
857
858        let mut audio = vec![0.0f32; config.block_size];
859        processor.process(&mut audio).unwrap();
860    }
861
862    #[test]
863    fn process_fixed_block_size() {
864        let (model, license_key) = load_test_model().unwrap();
865        let config = ProcessorConfig::optimal(&model);
866
867        let mut processor = Processor::new(&model, &license_key)
868            .unwrap()
869            .with_config(&config)
870            .unwrap();
871
872        let mut audio = vec![0.0f32; config.block_size];
873        processor.process(&mut audio).unwrap();
874    }
875
876    #[test]
877    fn process_variable_block_size() {
878        let (model, license_key) = load_test_model().unwrap();
879        let config = ProcessorConfig::optimal(&model).with_variable_block_size(true);
880
881        let mut processor = Processor::new(&model, &license_key)
882            .unwrap()
883            .with_config(&config)
884            .unwrap();
885
886        let mut audio = vec![0.0f32; config.block_size];
887        processor.process(&mut audio).unwrap();
888
889        let mut audio = vec![0.0f32; 20];
890        processor.process(&mut audio).unwrap();
891    }
892
893    #[test]
894    fn process_variable_block_size_fails_when_disabled() {
895        let (model, license_key) = load_test_model().unwrap();
896        let config = ProcessorConfig::optimal(&model);
897
898        let mut processor = Processor::new(&model, &license_key)
899            .unwrap()
900            .with_config(&config)
901            .unwrap();
902
903        let mut audio = vec![0.0f32; config.block_size];
904        processor.process(&mut audio).unwrap();
905
906        let mut audio = vec![0.0f32; 20];
907        let result = processor.process(&mut audio);
908        assert_eq!(result, Err(AicError::AudioConfigMismatch));
909    }
910
911    #[test]
912    fn model_can_be_dropped_after_creating_processor() {
913        let (model, license_key) = load_test_model().unwrap();
914        let config = ProcessorConfig::optimal(&model);
915
916        let mut processor = Processor::new(&model, &license_key)
917            .unwrap()
918            .with_config(&config)
919            .unwrap();
920        drop(model); // Inside of the SDK an Arc-Pointer to `Model` is stored in Processor, so it won't be de-allocated
921
922        let mut audio = vec![0.0f32; config.block_size];
923        processor.process(&mut audio).unwrap();
924    }
925
926    #[test]
927    fn processor_is_send_and_sync() {
928        // Compile-time check that Processor implements Send and Sync.
929        // This ensures the processor can be safely moved to another thread.
930        fn assert_send<T: Send>() {}
931        fn assert_sync<T: Send>() {}
932
933        assert_send::<Processor>();
934        assert_sync::<Processor>();
935    }
936
937    struct MyModel {
938        _model: Model<'static>,
939        _processor: Processor<'static>,
940    }
941
942    impl MyModel {
943        pub fn new() -> Self {
944            let (model, license_key) = load_test_model().unwrap();
945            let processor = Processor::new(&model, &license_key)
946                .unwrap()
947                .with_config(&ProcessorConfig::optimal(&model))
948                .unwrap();
949            MyModel {
950                _model: model,
951                _processor: processor,
952            }
953        }
954    }
955
956    #[test]
957    fn can_create_self_referential_structs_with_statics() {
958        let _model = MyModel::new();
959    }
960}
961
962#[doc(hidden)]
963mod _compile_fail_tests {
964    //! Compile-fail regression: a `Processor`'s model buffer must not be dropped before the processor.
965    //!
966    //! ```rust,compile_fail
967    //! use aic_sdk::{Model, Processor, ProcessorConfig};
968    //!
969    //! fn main() {
970    //!     let buffer = vec![0u8; 64];
971    //!     let model = Model::from_buffer(&buffer).unwrap();
972    //!     let config = ProcessorConfig::optimal(&model);
973    //!
974    //!     let mut processor = Processor::new(&model, "license")
975    //!         .unwrap()
976    //!         .with_config(&config)
977    //!         .unwrap();
978    //!
979    //!     drop(model); // Model can be dropped without issues
980    //!
981    //!     drop(buffer); // This should fail to compile
982    //!
983    //!     let mut audio = vec![0.0f32; config.block_size];
984    //!     processor.process(&mut audio).unwrap();
985    //! }
986    //! ```
987}