aic-sdk 0.21.3

ai-coustics Speech Enhancement SDK
Documentation
use crate::error::*;

use aic_sdk_sys::{AicVadParameter::*, *};

/// Configurable parameters for Voice Activity Detection.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VadParameter {
    /// Controls for how long the VAD continues to detect speech after the audio signal
    /// no longer contains speech.
    ///
    /// This affects the stability of speech detected -> not detected transitions.
    ///
    /// The VAD reports speech detected if the audio signal contained speech in at least 50%
    /// of the frames processed in the last `speech_hold_duration * 2` seconds.
    ///
    /// For example, if `speech_hold_duration` is set to 0.5 seconds and the VAD stops detecting speech
    /// in the audio signal, the VAD will continue to report speech for 0.5 seconds assuming the
    /// VAD does not detect speech again during that period. If a few frames of speech are detected
    /// during that period, those frames will be included in the 50% calculation, which will extend
    /// the speech detection period until the 50% threshold is no longer met.
    ///
    /// NOTE: The VAD returns a value per processed buffer, so this duration is rounded
    /// to the closest model window length. For example, if the model has a processing window
    /// length of 10 ms, the VAD will round up/down to the closest multiple of 10 ms.
    /// Because of this, this parameter may return a different value than the one it was last set to.
    ///
    /// **Range:** 0.0 to 300x model window length (value in seconds)
    ///
    /// **Default:** 0.03 (30 ms)
    SpeechHoldDuration,
    /// Controls the sensitivity of the VAD.
    ///
    /// There are two kinds of VADs offered by the SDK:
    ///
    /// - **VAD models** (e.g. Quail VAD): models trained specifically for voice activity
    ///   detection. They output a probability of speech presence for each processed audio buffer
    ///   (1.0 = certain speech, 0.0 = certain no speech). The probability is compared against the
    ///   sensitivity threshold to decide whether speech is detected.
    /// - **Energy-based VADs** of speech enhancement models (e.g. Quail, Rook): these models
    ///   filter out background noise and enhance speech but do not explicitly output a VAD
    ///   decision. The SDK derives one from the energy remaining in the signal after
    ///   enhancement. The energy threshold is `10 ^ (-sensitivity)`, so higher sensitivity
    ///   values require less energy in the signal, resulting in more aggressive speech
    ///   detection.
    ///
    /// A value above the threshold triggers a speech-detected decision.
    ///
    /// **Range:**
    /// - VAD models: 0.0 to 1.0
    /// - Energy-based VADs: 1.0 to 15.0
    ///
    /// **Default:** model-specific
    Sensitivity,
    /// Controls for how long speech needs to be present in the audio signal before
    /// the VAD considers it speech.
    ///
    /// This affects the stability of speech not detected -> detected transitions.
    ///
    /// NOTE: The VAD returns a value per processed buffer, so this duration is rounded
    /// to the closest model window length. For example, if the model has a processing window
    /// length of 10 ms, the VAD will round up/down to the closest multiple of 10 ms.
    /// Because of this, this parameter may return a different value than the one it was last set to.
    ///
    /// **Range:** 0.0 to 1.0 (value in seconds)
    ///
    /// **Default:** 0.0
    MinimumSpeechDuration,
}

impl From<VadParameter> for AicVadParameter::Type {
    fn from(parameter: VadParameter) -> Self {
        match parameter {
            VadParameter::SpeechHoldDuration => AIC_VAD_PARAMETER_SPEECH_HOLD_DURATION,
            VadParameter::Sensitivity => AIC_VAD_PARAMETER_SENSITIVITY,
            VadParameter::MinimumSpeechDuration => AIC_VAD_PARAMETER_MINIMUM_SPEECH_DURATION,
        }
    }
}

/// Voice Activity Detector backed by an ai-coustics speech enhancement model.
///
/// The VAD works automatically using the enhanced audio output of the processor
/// that created the VAD.
///
/// **Important:** If the backing processor is destroyed, the VAD instance will stop
/// producing new data.
///
/// # Example
///
/// ```rust,no_run
/// use aic_sdk::{Model, Processor};
///
/// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
/// let model = Model::from_file("/path/to/model.aicmodel")?;
/// let processor = Processor::new(&model, &license_key)?;
/// let vad = processor.vad_context();
/// # Ok::<(), aic_sdk::AicError>(())
/// ```
pub struct VadContext {
    /// Raw pointer to the C VAD structure
    inner: *mut AicVadContext,
}

impl VadContext {
    /// Creates a new VAD context.
    pub(crate) fn new(vad_ptr: *mut AicVadContext) -> Self {
        Self { inner: vad_ptr }
    }

    fn as_const_ptr(&self) -> *const AicVadContext {
        self.inner as *const AicVadContext
    }

    /// Returns the VAD's prediction.
    ///
    /// # Latency
    ///
    /// The latency of the VAD prediction is equal to the backing processor's processing latency,
    /// reported by [`ProcessorContext::output_delay`](crate::ProcessorContext::output_delay).
    /// The prediction lags its input by that many samples, even for a dedicated VAD model
    /// whose audio buffer passes through untouched.
    ///
    /// Align speech decisions to the input timeline using that delay.
    ///
    /// If the backing processor stops being processed, the VAD will not update its prediction.
    pub fn is_speech_detected(&self) -> bool {
        let mut value: bool = false;
        // SAFETY:
        // - `self.as_const_ptr()` is a valid pointer to a live VAD context.
        // - `value` points to stack storage for output.
        // - This function can be called from any thread, so we only borrow `&self`.
        let error_code =
            unsafe { aic_vad_context_is_speech_detected(self.as_const_ptr(), &mut value) };

        // This should never fail
        assert!(handle_error(error_code).is_ok());
        value
    }

    /// Returns the raw prediction of the VAD, without any processing.
    ///
    /// In contrast to the output of [`VadContext::is_speech_detected`],
    /// the output of this function is the model's direct prediction without
    /// going through the SDK's VAD post-processing (i.e. speech hold duration,
    /// sensitivity thresholding, etc.).
    ///
    /// This value may be used to build other abstractions on top of this data.
    ///
    /// # Note
    ///
    /// This value is only useful when using a VAD model. When using an energy-based VAD,
    /// the raw prediction is set to 1.0 or 0.0 depending on whether [`VadContext::is_speech_detected`]
    /// is true or false.
    ///
    /// # Latency
    ///
    /// The latency of the VAD prediction is equal to the backing processor's processing latency,
    /// reported by [`ProcessorContext::output_delay`](crate::ProcessorContext::output_delay).
    /// The prediction lags its input by that many samples, even for a dedicated VAD model
    /// whose audio buffer passes through untouched.
    ///
    /// Align speech decisions to the input timeline using that delay.
    ///
    /// If the backing processor stops being processed, the VAD will not update its prediction.
    pub fn raw_vad_probability(&self) -> f32 {
        let mut value: f32 = 0.0;
        // SAFETY:
        // - `self.as_const_ptr()` is a valid pointer to a live VAD context.
        // - `value` points to stack storage for output.
        // - This function can be called from any thread, so we only borrow `&self`.
        let error_code =
            unsafe { aic_vad_context_get_raw_vad_probability(self.as_const_ptr(), &mut value) };

        // This should never fail
        assert!(handle_error(error_code).is_ok());
        value
    }

    /// Modifies a VAD parameter.
    ///
    /// # Arguments
    ///
    /// - `parameter` - Parameter to modify
    /// - `value` - New parameter value. See parameter documentation for ranges
    ///
    /// # Returns
    ///
    /// Returns `Ok(())` on success or an `AicError` if the parameter cannot be set.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// # use aic_sdk::{Model, Processor, VadParameter};
    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
    /// # let processor = Processor::new(&model, &license_key)?;
    /// # let vad = processor.vad_context();
    /// vad.set_parameter(VadParameter::SpeechHoldDuration, 0.08)?;
    /// vad.set_parameter(VadParameter::Sensitivity, 5.0)?;
    /// # Ok::<(), aic_sdk::AicError>(())
    /// ```
    pub fn set_parameter(&self, parameter: VadParameter, value: f32) -> Result<(), AicError> {
        // SAFETY:
        // - `self.as_const_ptr()` is a live VAD context pointer.
        // - This function can be called from any thread, so we only borrow `&self`.
        let error_code =
            unsafe { aic_vad_context_set_parameter(self.as_const_ptr(), parameter.into(), value) };
        handle_error(error_code)
    }

    /// Retrieves the current value of a VAD parameter.
    ///
    /// # Arguments
    ///
    /// - `parameter` - Parameter to query
    ///
    /// # Returns
    ///
    /// Returns `Ok(value)` containing the current parameter value, or an `AicError` if the query fails.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// # use aic_sdk::{Model, Processor, VadParameter};
    /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
    /// # let model = Model::from_file("/path/to/model.aicmodel")?;
    /// # let processor = Processor::new(&model, &license_key)?;
    /// # let vad = processor.vad_context();
    /// let sensitivity = vad.parameter(VadParameter::Sensitivity)?;
    /// println!("Current sensitivity: {sensitivity}");
    /// # Ok::<(), aic_sdk::AicError>(())
    /// ```
    pub fn parameter(&self, parameter: VadParameter) -> Result<f32, AicError> {
        let mut value: f32 = 0.0;
        // SAFETY:
        // - `self.as_const_ptr()` is a valid pointer to a live VAD context.
        // - `value` points to stack storage for output.
        // - This function can be called from any thread, so we only borrow `&self`.
        let error_code = unsafe {
            aic_vad_context_get_parameter(self.as_const_ptr(), parameter.into(), &mut value)
        };
        handle_error(error_code)?;
        Ok(value)
    }
}

impl Drop for VadContext {
    fn drop(&mut self) {
        if !self.inner.is_null() {
            // SAFETY:
            // - `self.inner` was allocated by the SDK and is still owned by this wrapper.
            // - This function can be called from any thread; `drop` has exclusive
            //   access to this VAD context handle.
            unsafe { aic_vad_context_destroy(self.inner) };
        }
    }
}

// Safety: The underlying C library should be thread-safe for individual VadContext instances
unsafe impl Send for VadContext {}
unsafe impl Sync for VadContext {}