Skip to main content

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}