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