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}