aic_sdk/vad.rs
1use crate::{
2 error::*,
3 model::Model,
4 processor::{OtelConfig, ProcessorConfig},
5};
6
7use aic_sdk_sys::{AicVadParameter::*, *};
8
9use std::{ffi::CString, marker::PhantomData, ptr};
10
11/// Configurable parameters for Voice Activity Detection.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum VadParameter {
14 /// Controls for how long the VAD continues to detect speech after the audio signal
15 /// no longer contains speech.
16 ///
17 /// This affects the stability of speech detected -> not detected transitions.
18 ///
19 /// The VAD reports speech detected if the audio signal contained speech in at least 50%
20 /// of the blocks processed in the last `speech_hold_duration * 2` seconds.
21 ///
22 /// For example, if `speech_hold_duration` is set to 0.5 seconds and the VAD stops detecting speech
23 /// in the audio signal, the VAD will continue to report speech for 0.5 seconds assuming the
24 /// VAD does not detect speech again during that period. If a few blocks of speech are detected
25 /// during that period, those blocks will be included in the 50% calculation, which will extend
26 /// the speech detection period until the 50% threshold is no longer met.
27 ///
28 /// NOTE: The VAD returns a value per processed audio block, so this duration is rounded
29 /// to the closest model window length. For example, if the model has a processing window
30 /// length of 10 ms, the VAD will round up/down to the closest multiple of 10 ms.
31 /// Because of this, this parameter may return a different value than the one it was last set to.
32 ///
33 /// **Range:** 0.0 to 300x model window length (value in seconds)
34 ///
35 /// **Default:** model-specific
36 SpeechHoldDuration,
37 /// Controls the sensitivity of the VAD.
38 ///
39 /// VAD models output a probability of speech presence for each processed audio block,
40 /// 1.0 being the model is certain speech is present and 0.0 being the model is certain
41 /// speech is not present. The probability is compared against the sensitivity threshold
42 /// to determine if speech is detected.
43 ///
44 /// A value above the threshold will trigger a "speech detected" decision.
45 ///
46 /// **Range:** 0.0 to 1.0
47 ///
48 /// **Default:** model-specific
49 Sensitivity,
50 /// Controls for how long speech needs to be present in the audio signal before
51 /// the VAD considers it speech.
52 ///
53 /// This affects the stability of speech not detected -> detected transitions.
54 ///
55 /// NOTE: The VAD returns a value per processed audio block, so this duration is rounded
56 /// to the closest model window length. For example, if the model has a processing window
57 /// length of 10 ms, the VAD will round up/down to the closest multiple of 10 ms.
58 /// Because of this, this parameter may return a different value than the one it was last set to.
59 ///
60 /// **Range:** 0.0 to 1.0 (value in seconds)
61 ///
62 /// **Default:** model-specific
63 MinimumSpeechDuration,
64}
65
66impl From<VadParameter> for AicVadParameter::Type {
67 fn from(parameter: VadParameter) -> Self {
68 match parameter {
69 VadParameter::SpeechHoldDuration => AIC_VAD_PARAMETER_SPEECH_HOLD_DURATION,
70 VadParameter::Sensitivity => AIC_VAD_PARAMETER_SENSITIVITY,
71 VadParameter::MinimumSpeechDuration => AIC_VAD_PARAMETER_MINIMUM_SPEECH_DURATION,
72 }
73 }
74}
75
76/// High-level wrapper for the ai-coustics voice activity detector.
77///
78/// A `Vad` is created from a VAD model (e.g. `vad-2.1-xxs-16khz`). Enhancement models
79/// cannot be used for voice activity detection; pass them to a [`Processor`](crate::Processor)
80/// instead.
81///
82/// Feed the audio to be examined to [`Vad::process`]. The audio is not modified, it only
83/// updates the detector's prediction, which is read through a [`VadContext`].
84///
85/// # Example
86///
87/// ```rust,no_run
88/// use aic_sdk::{Model, ProcessorConfig, Vad};
89///
90/// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
91/// let model = Model::from_file("/path/to/vad_model.aicmodel")?;
92/// let config = ProcessorConfig::optimal(&model);
93///
94/// let mut vad = Vad::new(&model, &license_key)?.with_config(&config)?;
95/// let vad_ctx = vad.context();
96///
97/// let audio_block = vec![0.0f32; config.block_size];
98/// vad.process(&audio_block)?;
99///
100/// if vad_ctx.is_speech_detected() {
101/// println!("Speech detected!");
102/// }
103/// # Ok::<(), aic_sdk::AicError>(())
104/// ```
105pub struct Vad<'a> {
106 /// Raw pointer to the C VAD structure
107 inner: *mut AicVad,
108 /// Whether `initialize` has been called
109 initialized: bool,
110 /// Marker to tie the lifetime of the VAD to the lifetime of the model's weights
111 marker: PhantomData<&'a [u8]>,
112}
113
114impl<'a> Vad<'a> {
115 /// Creates a new voice activity detector instance.
116 ///
117 /// Multiple VAD instances can be created to process different audio streams simultaneously.
118 ///
119 /// The same [`Model`] may be passed to this function more than once: each call creates an
120 /// independent VAD that shares the underlying model data internally.
121 ///
122 /// # Arguments
123 ///
124 /// * `model` - The loaded model instance. Must be a VAD model, otherwise
125 /// [`AicError::ModelTypeUnsupported`] is returned.
126 /// * `license_key` - license key for the ai-coustics SDK
127 /// (generate your key at [developers.ai-coustics.com](https://developers.ai-coustics.com/))
128 ///
129 /// # Returns
130 ///
131 /// Returns a `Result` containing the new `Vad` instance or an [`AicError`] if creation fails.
132 ///
133 /// # Example
134 ///
135 /// ```rust,no_run
136 /// # use aic_sdk::{Model, Vad};
137 /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
138 /// let model = Model::from_file("/path/to/vad_model.aicmodel")?;
139 /// let vad = Vad::new(&model, &license_key)?;
140 /// # Ok::<(), aic_sdk::AicError>(())
141 /// ```
142 pub fn new(model: &Model<'a>, license_key: &str) -> Result<Self, AicError> {
143 Self::create(model, license_key, None)
144 }
145
146 /// Creates a new voice activity detector instance with explicit OpenTelemetry configuration.
147 ///
148 /// If provided, telemetry will be sent according to the provided configuration. Otherwise
149 /// it will be configured according to the runtime environment.
150 ///
151 /// This overrides the SDK's environment-based telemetry defaults (e.g.
152 /// `AIC_SDK_OTEL_ENABLE`) for this VAD.
153 ///
154 /// # Example
155 ///
156 /// ```rust,no_run
157 /// # use aic_sdk::{Model, OtelConfig, Vad};
158 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
159 /// let model = Model::from_file("/path/to/vad_model.aicmodel")?;
160 /// let otel = OtelConfig::enabled();
161 ///
162 /// let vad = Vad::with_otel_config(&model, &license_key, &otel)?;
163 /// # Ok::<(), aic_sdk::AicError>(())
164 /// ```
165 pub fn with_otel_config(
166 model: &Model<'a>,
167 license_key: &str,
168 otel_config: &OtelConfig,
169 ) -> Result<Self, AicError> {
170 Self::create(model, license_key, Some(otel_config))
171 }
172
173 fn create(
174 model: &Model<'a>,
175 license_key: &str,
176 otel_config: Option<&OtelConfig>,
177 ) -> Result<Self, AicError> {
178 // Set the wrapper ID as soon as the user attempts to instantiate a VAD.
179 // SAFETY: `2` is the wrapper ID assigned to this Rust SDK.
180 unsafe { crate::set_sdk_id(2) };
181
182 // Session ID must outlive the FFI call so its pointer stays valid.
183 let c_session_id = otel_config
184 .and_then(|o| o.session_id.as_deref())
185 .map(CString::new)
186 .transpose()
187 .map_err(|_| AicError::Internal)?;
188
189 let c_otel = otel_config.map(|o| AicOtelConfig {
190 enable: o.enable,
191 session_id: c_session_id.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
192 export_interval_ms: o.export_interval_ms,
193 });
194 let c_otel_ptr = c_otel
195 .as_ref()
196 .map_or(ptr::null(), |o| o as *const AicOtelConfig);
197
198 let mut vad_ptr: *mut AicVad = ptr::null_mut();
199 let c_license_key =
200 CString::new(license_key).map_err(|_| AicError::LicenseFormatInvalid)?;
201
202 // SAFETY:
203 // - `vad_ptr` points to stack storage for output.
204 // - `model` is a valid SDK model pointer for the duration of the call.
205 // - `c_license_key` is a null-terminated CString.
206 // - `c_otel_ptr` is either null or points to a valid `AicOtelConfig` whose
207 // `session_id` field (if non-null) outlives this call.
208 // - The output pointer is local to this call and not aliased.
209 let error_code = unsafe {
210 aic_vad_create(
211 &mut vad_ptr,
212 model.as_const_ptr(),
213 c_license_key.as_ptr(),
214 c_otel_ptr,
215 )
216 };
217
218 handle_error(error_code)?;
219
220 // This should never happen if the C library is well-behaved, but let's be defensive
221 assert!(
222 !vad_ptr.is_null(),
223 "C library returned success but null pointer"
224 );
225
226 Ok(Self {
227 inner: vad_ptr,
228 initialized: false,
229 marker: PhantomData,
230 })
231 }
232
233 /// Initializes the VAD with the given configuration.
234 ///
235 /// This is a convenience method that calls [`Vad::initialize`] internally and returns `self`.
236 /// The VAD is immediately ready to process audio after calling this method, so you don't
237 /// need to call [`Vad::initialize`] separately.
238 ///
239 /// # Arguments
240 ///
241 /// * `config` - Audio processing configuration
242 ///
243 /// # Returns
244 ///
245 /// Returns `Ok(Self)` with the initialized VAD, or an [`AicError`] if initialization fails.
246 ///
247 /// # Example
248 ///
249 /// ```rust,no_run
250 /// # use aic_sdk::{Model, ProcessorConfig, Vad};
251 /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
252 /// let model = Model::from_file("/path/to/vad_model.aicmodel")?;
253 /// let config = ProcessorConfig::optimal(&model);
254 ///
255 /// let mut vad = Vad::new(&model, &license_key)?.with_config(&config)?;
256 ///
257 /// // VAD is ready to use - no need to call initialize()
258 /// let audio_block = vec![0.0f32; config.block_size];
259 /// vad.process(&audio_block)?;
260 /// # Ok::<(), aic_sdk::AicError>(())
261 /// ```
262 pub fn with_config(mut self, config: &ProcessorConfig) -> Result<Self, AicError> {
263 self.initialize(config)?;
264 Ok(self)
265 }
266
267 /// Configures the VAD for specific audio settings.
268 ///
269 /// This function must be called before processing any audio.
270 /// For the most frequent prediction updates, use the sample rate and block size returned by
271 /// [`Model::optimal_sample_rate`] and [`Model::optimal_block_size`].
272 ///
273 /// # Arguments
274 ///
275 /// * `config` - Audio processing configuration
276 ///
277 /// # Returns
278 ///
279 /// Returns `Ok(())` on success or an [`AicError`] if initialization fails.
280 ///
281 /// # Warning
282 /// Do not call from audio processing threads as this allocates memory.
283 ///
284 /// # Example
285 ///
286 /// ```rust,no_run
287 /// # use aic_sdk::{Model, ProcessorConfig, Vad};
288 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
289 /// # let model = Model::from_file("/path/to/vad_model.aicmodel")?;
290 /// # let mut vad = Vad::new(&model, &license_key)?;
291 /// let config = ProcessorConfig::optimal(&model);
292 /// vad.initialize(&config)?;
293 /// # Ok::<(), aic_sdk::AicError>(())
294 /// ```
295 pub fn initialize(&mut self, config: &ProcessorConfig) -> Result<(), AicError> {
296 // SAFETY:
297 // - `self.inner` is a valid pointer to a live VAD.
298 // - This function is not thread-safe, so we borrow `&mut self`.
299 let error_code = unsafe {
300 aic_vad_initialize(
301 self.inner,
302 config.sample_rate,
303 config.block_size,
304 config.variable_block_size,
305 )
306 };
307
308 handle_error(error_code)?;
309 self.initialized = true;
310 Ok(())
311 }
312
313 /// Processes mono audio and updates the VAD prediction.
314 ///
315 /// This function does not modify the input audio buffer. Read the prediction through a
316 /// [`VadContext`].
317 ///
318 /// # Recommendation
319 ///
320 /// When enhancement and VAD run together, pass the original input audio here, not the output
321 /// of [`Processor::process`](crate::Processor::process). Enhancement is designed to change the
322 /// signal, so running the VAD on its output means detecting speech in audio that no longer
323 /// matches what the VAD model expects, and it stacks the processor's audio delay on top of the
324 /// VAD's prediction delay. Because this function does not modify its input, calling it on the
325 /// same buffer before `Processor::process` is enough:
326 ///
327 /// ```rust,no_run
328 /// # use aic_sdk::{Model, Processor, ProcessorConfig, Vad};
329 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
330 /// # let model = Model::from_file("/path/to/model.aicmodel")?;
331 /// # let vad_model = Model::from_file("/path/to/vad_model.aicmodel")?;
332 /// # let config = ProcessorConfig::optimal(&model);
333 /// # let mut processor = Processor::new(&model, &license_key)?.with_config(&config)?;
334 /// # let mut vad = Vad::new(&vad_model, &license_key)?.with_config(&config)?;
335 /// # let mut audio = vec![0.0f32; config.block_size];
336 /// vad.process(&audio)?; // reads the block, does not modify it
337 /// processor.process(&mut audio)?; // enhances the block in-place
338 /// # Ok::<(), aic_sdk::AicError>(())
339 /// ```
340 ///
341 /// # Arguments
342 ///
343 /// * `audio` - Mono audio block to examine. Must match `block_size` from initialization, or
344 /// if `variable_block_size` was enabled, must be less than or equal to `block_size`.
345 ///
346 /// # Returns
347 ///
348 /// Returns `Ok(())` on success or an [`AicError`] if processing fails.
349 ///
350 /// # Real-time safety
351 ///
352 /// Real-time safe. Can be called from audio processing threads.
353 ///
354 /// # Example
355 ///
356 /// ```rust,no_run
357 /// # use aic_sdk::{Model, ProcessorConfig, Vad};
358 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
359 /// # let model = Model::from_file("/path/to/vad_model.aicmodel")?;
360 /// # let mut vad = Vad::new(&model, &license_key)?;
361 /// let config = ProcessorConfig::optimal(&model);
362 /// vad.initialize(&config)?;
363 /// let audio = vec![0.0f32; config.block_size];
364 /// vad.process(&audio)?;
365 /// # Ok::<(), aic_sdk::AicError>(())
366 /// ```
367 pub fn process(&mut self, audio: &[f32]) -> Result<(), AicError> {
368 if !self.initialized {
369 return Err(AicError::NotInitialized);
370 }
371
372 let audio_len = audio.len();
373
374 // SAFETY:
375 // - `self.inner` is a valid pointer to a live VAD.
376 // - `audio` points to a contiguous, readable f32 slice of length `audio_len` that the
377 // C library only reads from.
378 // - This function is not thread-safe, so we borrow `&mut self`.
379 let error_code = unsafe { aic_vad_process(self.inner, audio.as_ptr(), audio_len) };
380
381 handle_error(error_code)
382 }
383
384 /// Creates a [`VadContext`] instance.
385 /// This can be used to read the prediction and to control all parameters and other
386 /// settings of the VAD.
387 ///
388 /// All handles created from a given VAD reference the same VAD instance.
389 ///
390 /// # Example
391 ///
392 /// ```rust,no_run
393 /// # use aic_sdk::{Model, Vad};
394 /// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
395 /// let model = Model::from_file("/path/to/vad_model.aicmodel")?;
396 /// let vad = Vad::new(&model, &license_key)?;
397 /// let vad_ctx = vad.context();
398 /// # Ok::<(), aic_sdk::AicError>(())
399 /// ```
400 pub fn context(&self) -> VadContext {
401 let mut context_ptr: *mut AicVadContext = ptr::null_mut();
402
403 // SAFETY:
404 // - `context_ptr` is valid output storage and not aliased.
405 // - `self.as_const_ptr()` is a live VAD pointer.
406 // - This function can be called from any thread and may run while the
407 // VAD is in use, so we only borrow `&self`.
408 let error_code = unsafe { aic_vad_context_create(&mut context_ptr, self.as_const_ptr()) };
409
410 // This should never fail
411 assert!(handle_error(error_code).is_ok());
412
413 // This should never happen if the C library is well-behaved, but let's be defensive
414 assert!(
415 !context_ptr.is_null(),
416 "C library returned success but null pointer"
417 );
418
419 VadContext::new(context_ptr)
420 }
421
422 /// Terminates the telemetry session associated with this VAD.
423 ///
424 /// Once the request has been handled, the VAD is no longer allowed to process audio.
425 ///
426 /// This function is meant to be used in lifecycle management events.
427 /// A telemetry session is automatically stopped when a VAD is destroyed.
428 /// However, in cases where this SDK is integrated with languages with automatic memory
429 /// management, object deallocation could be delayed. Use this function to terminate
430 /// the session explicitly.
431 ///
432 /// This function blocks until the telemetry session is terminated, unless another
433 /// session is still alive. In that case, this function returns early and termination
434 /// happens asynchronously. This keeps lifecycle management smooth while ensuring
435 /// all sessions are closed when the last VAD is terminated.
436 ///
437 /// # Returns
438 ///
439 /// Returns `Ok(())` on success or an [`AicError`] if termination cannot be requested.
440 ///
441 /// # Real-time safety
442 ///
443 /// This function is not real-time safe. It may block until the session is terminated.
444 /// Avoid calling it from audio threads.
445 ///
446 /// # Example
447 ///
448 /// ```rust,no_run
449 /// # use aic_sdk::{Model, Vad};
450 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
451 /// # let model = Model::from_file("/path/to/vad_model.aicmodel")?;
452 /// let mut vad = Vad::new(&model, &license_key)?;
453 /// vad.terminate_session()?;
454 /// # Ok::<(), aic_sdk::AicError>(())
455 /// ```
456 pub fn terminate_session(&mut self) -> Result<(), AicError> {
457 // SAFETY:
458 // - `self.inner` is a valid pointer to a live VAD.
459 // - This function must not run concurrently with any other call taking the same
460 // VAD handle, so we borrow `&mut self`.
461 let error_code = unsafe { aic_vad_terminate_session(self.inner) };
462 handle_error(error_code)
463 }
464
465 fn as_const_ptr(&self) -> *const AicVad {
466 self.inner as *const AicVad
467 }
468}
469
470impl<'a> Drop for Vad<'a> {
471 fn drop(&mut self) {
472 if !self.inner.is_null() {
473 // SAFETY:
474 // - `self.inner` was allocated by the SDK and is still owned by this wrapper.
475 // - This function is not thread-safe with concurrent VAD use, but
476 // `drop` has exclusive access to `self`.
477 unsafe { aic_vad_destroy(self.inner) };
478 }
479 }
480}
481
482// SAFETY: Everything in Vad is Send, with the exception of the inner raw pointer.
483// The Vad only uses the raw pointer according to the safety contracts of the
484// unsafe APIs that require the pointer, and the Vad does not expose access to the
485// raw pointer in any of its methods. Therefore, it is safe to implement Send for Vad.
486unsafe impl<'a> Send for Vad<'a> {}
487
488// SAFETY: Vad does not expose any interior mutability. The SDK functions that are documented
489// as not thread-safe (`aic_vad_initialize`, `aic_vad_process`, `aic_vad_terminate_session`,
490// `aic_vad_destroy`) are only reachable through methods that take `&mut self` or through `drop`,
491// so Rust's borrow rules serialize them. The only method that takes `&self` (`context`) just
492// creates a new context handle from a const VAD pointer, which is safe to do while the VAD is in
493// use on another thread. Therefore, it is safe to implement Sync for Vad.
494unsafe impl<'a> Sync for Vad<'a> {}
495
496/// Thread-safe control handle for a [`Vad`].
497///
498/// Create one with [`Vad::context`]. Every method on this type maps to an SDK function that
499/// can be called from any thread, so a context can be moved to another thread to read the
500/// prediction, read and write parameters, query the prediction delay, or reset the VAD while audio is
501/// being processed elsewhere.
502///
503/// All handles created from a given VAD reference the same VAD instance.
504///
505/// **Important:** If the backing [`Vad`] is dropped, the VAD stops producing new data. Dropping
506/// the context does not destroy the VAD.
507///
508/// # Example
509///
510/// ```rust,no_run
511/// use aic_sdk::{Model, Vad};
512///
513/// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
514/// let model = Model::from_file("/path/to/vad_model.aicmodel")?;
515/// let vad = Vad::new(&model, &license_key)?;
516/// let vad_ctx = vad.context();
517/// # Ok::<(), aic_sdk::AicError>(())
518/// ```
519pub struct VadContext {
520 /// Raw pointer to the C VAD context structure
521 inner: *mut AicVadContext,
522}
523
524impl VadContext {
525 /// Creates a new VAD context.
526 pub(crate) fn new(context_ptr: *mut AicVadContext) -> Self {
527 Self { inner: context_ptr }
528 }
529
530 fn as_const_ptr(&self) -> *const AicVadContext {
531 self.inner as *const AicVadContext
532 }
533
534 /// Returns the VAD's prediction.
535 ///
536 /// # Latency
537 ///
538 /// The latency of the VAD prediction is equal to the backing VAD's processing latency,
539 /// reported by [`VadContext::prediction_delay`]. The prediction lags its input by that many
540 /// samples.
541 ///
542 /// Align speech decisions to the input timeline using that delay.
543 ///
544 /// If the backing VAD stops being processed, the VAD will not update its prediction.
545 pub fn is_speech_detected(&self) -> bool {
546 let mut value: bool = false;
547 // SAFETY:
548 // - `self.as_const_ptr()` is a valid pointer to a live VAD context.
549 // - `value` points to stack storage for output.
550 // - This function can be called from any thread, so we only borrow `&self`.
551 let error_code =
552 unsafe { aic_vad_context_is_speech_detected(self.as_const_ptr(), &mut value) };
553
554 // This should never fail
555 assert!(handle_error(error_code).is_ok());
556 value
557 }
558
559 /// Returns the raw prediction of the VAD, without any processing.
560 ///
561 /// In contrast to the output of [`VadContext::is_speech_detected`],
562 /// the output of this function is the model's direct prediction without
563 /// going through the SDK's VAD post-processing (i.e. speech hold duration,
564 /// sensitivity thresholding, etc.).
565 ///
566 /// This value may be used to build other abstractions on top of this data.
567 ///
568 /// # Latency
569 ///
570 /// The latency of the VAD prediction is equal to the backing VAD's processing latency,
571 /// reported by [`VadContext::prediction_delay`]. The prediction lags its input by that many
572 /// samples.
573 ///
574 /// Align speech decisions to the input timeline using that delay.
575 ///
576 /// If the backing VAD stops being processed, the VAD will not update its prediction.
577 pub fn raw_vad_probability(&self) -> f32 {
578 let mut value: f32 = 0.0;
579 // SAFETY:
580 // - `self.as_const_ptr()` is a valid pointer to a live VAD context.
581 // - `value` points to stack storage for output.
582 // - This function can be called from any thread, so we only borrow `&self`.
583 let error_code =
584 unsafe { aic_vad_context_get_raw_vad_probability(self.as_const_ptr(), &mut value) };
585
586 // This should never fail
587 assert!(handle_error(error_code).is_ok());
588 value
589 }
590
591 /// Modifies a VAD parameter.
592 ///
593 /// All parameters can be changed during audio processing.
594 /// This function can be called from any thread.
595 ///
596 /// # Arguments
597 ///
598 /// - `parameter` - Parameter to modify
599 /// - `value` - New parameter value. See parameter documentation for ranges
600 ///
601 /// # Returns
602 ///
603 /// Returns `Ok(())` on success or an `AicError` if the parameter cannot be set.
604 ///
605 /// # Example
606 ///
607 /// ```rust,no_run
608 /// # use aic_sdk::{Model, Vad, VadParameter};
609 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
610 /// # let model = Model::from_file("/path/to/vad_model.aicmodel")?;
611 /// # let vad = Vad::new(&model, &license_key)?;
612 /// # let vad_ctx = vad.context();
613 /// vad_ctx.set_parameter(VadParameter::SpeechHoldDuration, 0.08)?;
614 /// vad_ctx.set_parameter(VadParameter::Sensitivity, 0.5)?;
615 /// # Ok::<(), aic_sdk::AicError>(())
616 /// ```
617 pub fn set_parameter(&self, parameter: VadParameter, value: f32) -> Result<(), AicError> {
618 // SAFETY:
619 // - `self.as_const_ptr()` is a live VAD context pointer.
620 // - This function can be called from any thread, so we only borrow `&self`.
621 let error_code =
622 unsafe { aic_vad_context_set_parameter(self.as_const_ptr(), parameter.into(), value) };
623 handle_error(error_code)
624 }
625
626 /// Retrieves the current value of a VAD parameter.
627 ///
628 /// This function can be called from any thread.
629 ///
630 /// # Arguments
631 ///
632 /// - `parameter` - Parameter to query
633 ///
634 /// # Returns
635 ///
636 /// Returns `Ok(value)` containing the current parameter value, or an `AicError` if the query fails.
637 ///
638 /// # Example
639 ///
640 /// ```rust,no_run
641 /// # use aic_sdk::{Model, Vad, VadParameter};
642 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
643 /// # let model = Model::from_file("/path/to/vad_model.aicmodel")?;
644 /// # let vad = Vad::new(&model, &license_key)?;
645 /// # let vad_ctx = vad.context();
646 /// let sensitivity = vad_ctx.parameter(VadParameter::Sensitivity)?;
647 /// println!("Current sensitivity: {sensitivity}");
648 /// # Ok::<(), aic_sdk::AicError>(())
649 /// ```
650 pub fn parameter(&self, parameter: VadParameter) -> Result<f32, AicError> {
651 let mut value: f32 = 0.0;
652 // SAFETY:
653 // - `self.as_const_ptr()` is a valid pointer to a live VAD context.
654 // - `value` points to stack storage for output.
655 // - This function can be called from any thread, so we only borrow `&self`.
656 let error_code = unsafe {
657 aic_vad_context_get_parameter(self.as_const_ptr(), parameter.into(), &mut value)
658 };
659 handle_error(error_code)?;
660 Ok(value)
661 }
662
663 /// Returns the total VAD prediction delay in samples for the current audio configuration.
664 ///
665 /// This function provides the complete end-to-end latency of the VAD prediction, which
666 /// includes input reblocking, STFT, and model processing delay. Use this value to line up
667 /// VAD decisions with the input timeline.
668 ///
669 /// This delay is **not** applied to the audio: [`Vad::process`] leaves its input buffer
670 /// untouched. The value only describes how far behind its input the published prediction is.
671 ///
672 /// When enhancement and VAD run together, feed the VAD the original input audio rather than
673 /// the processor's output. This value is then the prediction's delay relative to that input,
674 /// and it is independent of the processor's
675 /// [`ProcessorContext::audio_delay`](crate::ProcessorContext::audio_delay).
676 ///
677 /// **Delay behavior:**
678 /// - **Before initialization:** Returns the base processing delay using the model's
679 /// optimal block size at its native sample rate
680 /// - **After initialization:** Returns the end-to-end VAD prediction delay at the
681 /// initialized sample rate, including the input-buffering latency of the configured
682 /// block size
683 ///
684 /// **Important:** The delay value is always expressed in samples at the sample rate
685 /// you configured during [`Vad::initialize`]. To convert to time units:
686 /// `delay_ms = (delay_samples * 1000) / sample_rate`
687 ///
688 /// **Note:** Using a block size different from the optimal value returned by
689 /// [`Model::optimal_block_size`], or enabling variable block sizes, can add input-buffering
690 /// latency before a new VAD prediction is published. That latency is included in the
691 /// reported delay.
692 ///
693 /// # Returns
694 ///
695 /// Returns the delay in samples.
696 ///
697 /// # Example
698 ///
699 /// ```rust,no_run
700 /// # use aic_sdk::{Model, Vad};
701 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
702 /// # let model = Model::from_file("/path/to/vad_model.aicmodel")?;
703 /// # let vad = Vad::new(&model, &license_key)?;
704 /// # let vad_ctx = vad.context();
705 /// let delay = vad_ctx.prediction_delay();
706 /// println!("VAD prediction delay: {delay} samples");
707 /// # Ok::<(), aic_sdk::AicError>(())
708 /// ```
709 pub fn prediction_delay(&self) -> usize {
710 let mut delay: usize = 0;
711 // SAFETY:
712 // - `self.as_const_ptr()` is a valid pointer to a live VAD context.
713 // - `delay` points to stack storage for output.
714 // - This function can be called from any thread, so we only borrow `&self`.
715 let error_code =
716 unsafe { aic_vad_context_get_prediction_delay(self.as_const_ptr(), &mut delay) };
717
718 // This should never fail. If it does, it's a bug in the SDK.
719 // `aic_vad_context_get_prediction_delay` is documented to always succeed if given
720 // valid pointers.
721 assert_success(
722 error_code,
723 "`aic_vad_context_get_prediction_delay` failed. This is a bug, please open an issue on GitHub for further investigation.",
724 );
725
726 delay
727 }
728
729 /// Clears all internal state and buffers. This also resets the VAD state, so the published
730 /// speech detection and raw probability values are cleared immediately.
731 ///
732 /// Call this when the audio stream is interrupted or when seeking
733 /// to prevent mispredictions from previous audio content.
734 ///
735 /// The VAD stays initialized to the configured settings.
736 ///
737 /// # Returns
738 ///
739 /// Returns `Ok(())` on success or an [`AicError`] if the reset fails.
740 ///
741 /// # Real-time safety
742 ///
743 /// Real-time safe. Can be called from audio processing threads.
744 ///
745 /// # Example
746 ///
747 /// ```rust,no_run
748 /// # use aic_sdk::{Model, Vad};
749 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
750 /// # let model = Model::from_file("/path/to/vad_model.aicmodel")?;
751 /// # let vad = Vad::new(&model, &license_key)?;
752 /// # let vad_ctx = vad.context();
753 /// vad_ctx.reset()?;
754 /// # Ok::<(), aic_sdk::AicError>(())
755 /// ```
756 pub fn reset(&self) -> Result<(), AicError> {
757 // SAFETY:
758 // - `self.as_const_ptr()` is a valid pointer to a live VAD context.
759 // - This function can be called from any thread, so we only borrow `&self`.
760 let error_code = unsafe { aic_vad_context_reset(self.as_const_ptr()) };
761 handle_error(error_code)
762 }
763
764 /// Replaces the bearer token on the running VAD.
765 ///
766 /// Use this when your license key is a JWT and needs to be refreshed
767 /// before it expires. Calling this with a renewed token lets you stay authenticated
768 /// without tearing down and recreating the VAD: audio processing continues
769 /// uninterrupted, the context handle stays valid, and the new token is used for all
770 /// subsequent authentication against the ai-coustics backend.
771 ///
772 /// In-place updates are only supported when both the originally configured key and the
773 /// new token are JWTs. Other license types cannot be swapped in this way.
774 ///
775 /// On any error the call is a no-op: the previously active token stays in use and the
776 /// telemetry session is unaffected (no backoff, no interruption to processing).
777 ///
778 /// On success the swap is applied immediately and is **not** gated on backend
779 /// acceptance. The token is validated locally for format only; if the backend later
780 /// rejects it (e.g. expired or revoked), the SDK retries it under backoff rather than
781 /// rolling back to the prior token, and audio processing is eventually disabled if no
782 /// accepted token arrives in time. Supplying a known-good token via this call during
783 /// that window recovers the session.
784 ///
785 /// Safe to call concurrently with [`Vad::process`] on the originating VAD.
786 ///
787 /// # Arguments
788 ///
789 /// * `token` - The new JWT to install.
790 ///
791 /// # Returns
792 ///
793 /// Returns `Ok(())` on success or an [`AicError`] if the update fails.
794 ///
795 /// # Real-time safety
796 ///
797 /// This function is not real-time safe. It locks a mutex and allocates memory.
798 /// Avoid calling it from audio threads.
799 ///
800 /// # Example
801 ///
802 /// ```rust,no_run
803 /// # use aic_sdk::{Model, Vad};
804 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
805 /// # let model = Model::from_file("/path/to/vad_model.aicmodel")?;
806 /// let vad = Vad::new(&model, &license_key)?;
807 /// let vad_ctx = vad.context();
808 /// let renewed_jwt = String::from("<JWT_BEARER_TOKEN>");
809 /// vad_ctx.update_bearer_token(&renewed_jwt)?;
810 /// # Ok::<(), aic_sdk::AicError>(())
811 /// ```
812 pub fn update_bearer_token(&self, token: &str) -> Result<(), AicError> {
813 let c_token = CString::new(token).map_err(|_| AicError::LicenseFormatInvalid)?;
814 // SAFETY:
815 // - `self.as_const_ptr()` is a valid pointer to a live VAD context.
816 // - `c_token` is a null-terminated CString that outlives the call.
817 // - This function can be called from any thread.
818 let error_code =
819 unsafe { aic_vad_context_update_bearer_token(self.as_const_ptr(), c_token.as_ptr()) };
820 handle_error(error_code)
821 }
822}
823
824impl Drop for VadContext {
825 fn drop(&mut self) {
826 if !self.inner.is_null() {
827 // SAFETY:
828 // - `self.inner` was allocated by the SDK and is still owned by this wrapper.
829 // - This function can be called from any thread; `drop` has exclusive
830 // access to this VAD context handle.
831 unsafe { aic_vad_context_destroy(self.inner) };
832 }
833 }
834}
835
836// Safety: The underlying C library should be thread-safe for individual VadContext instances
837unsafe impl Send for VadContext {}
838unsafe impl Sync for VadContext {}
839
840#[cfg(test)]
841mod tests {
842 use super::*;
843 use crate::test_support::{license_key, test_model_path};
844
845 /// Voice activity detection needs a dedicated VAD model; enhancement models are rejected.
846 const VAD_MODEL_ID: &str = "vad-2.1-xxs-16khz";
847 /// An enhancement model, used to check that `Vad` refuses one.
848 const ENHANCEMENT_MODEL_ID: &str = "rook-s-48khz";
849
850 fn load_vad_model() -> Model<'static> {
851 Model::from_file(test_model_path(VAD_MODEL_ID)).unwrap()
852 }
853
854 #[test]
855 fn vad_processes_audio_and_reports_prediction() {
856 let model = load_vad_model();
857 let config = ProcessorConfig::optimal(&model);
858
859 let mut vad = Vad::new(&model, &license_key())
860 .unwrap()
861 .with_config(&config)
862 .unwrap();
863
864 let vad_ctx = vad.context();
865 assert!(vad_ctx.prediction_delay() > 0);
866
867 let audio = vec![0.0f32; config.block_size];
868 vad.process(&audio).unwrap();
869
870 // Silence must not be reported as speech.
871 assert!(!vad_ctx.is_speech_detected());
872 assert!((0.0..=1.0).contains(&vad_ctx.raw_vad_probability()));
873
874 vad_ctx.reset().unwrap();
875 }
876
877 #[test]
878 fn vad_rejects_process_before_initialize() {
879 let model = load_vad_model();
880 let mut vad = Vad::new(&model, &license_key()).unwrap();
881
882 let audio = vec![0.0f32; 160];
883 assert_eq!(vad.process(&audio), Err(AicError::NotInitialized));
884 }
885
886 #[test]
887 fn vad_rejects_enhancement_model() {
888 let model = Model::from_file(test_model_path(ENHANCEMENT_MODEL_ID)).unwrap();
889
890 assert_eq!(
891 Vad::new(&model, &license_key()).err(),
892 Some(AicError::ModelTypeUnsupported)
893 );
894 }
895
896 #[test]
897 fn vad_parameters_round_trip() {
898 let model = load_vad_model();
899 let vad = Vad::new(&model, &license_key()).unwrap();
900 let vad_ctx = vad.context();
901
902 vad_ctx
903 .set_parameter(VadParameter::Sensitivity, 0.5)
904 .unwrap();
905 assert_eq!(vad_ctx.parameter(VadParameter::Sensitivity).unwrap(), 0.5);
906
907 // The sensitivity of a VAD model is a probability threshold.
908 assert_eq!(
909 vad_ctx.set_parameter(VadParameter::Sensitivity, 7.0),
910 Err(AicError::ParameterOutOfRange)
911 );
912 }
913
914 #[test]
915 fn vad_is_send_and_sync() {
916 // Compile-time check that Vad and VadContext implement Send and Sync.
917 fn assert_send<T: Send>() {}
918 fn assert_sync<T: Sync>() {}
919
920 assert_send::<Vad>();
921 assert_sync::<Vad>();
922 assert_send::<VadContext>();
923 assert_sync::<VadContext>();
924 }
925}
926
927#[doc(hidden)]
928mod _compile_fail_tests {
929 //! Compile-fail regression: a `Vad`'s model buffer must not be dropped before the VAD.
930 //!
931 //! ```rust,compile_fail
932 //! use aic_sdk::{Model, ProcessorConfig, Vad};
933 //!
934 //! fn main() {
935 //! let buffer = vec![0u8; 64];
936 //! let model = Model::from_buffer(&buffer).unwrap();
937 //! let config = ProcessorConfig::optimal(&model);
938 //!
939 //! let mut vad = Vad::new(&model, "license")
940 //! .unwrap()
941 //! .with_config(&config)
942 //! .unwrap();
943 //!
944 //! drop(model); // Model can be dropped without issues
945 //!
946 //! drop(buffer); // This should fail to compile
947 //!
948 //! let audio = vec![0.0f32; config.block_size];
949 //! vad.process(&audio).unwrap();
950 //! }
951 //! ```
952}