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}