use crate::{
error::*,
model::Model,
processor::{OtelConfig, ProcessorConfig},
};
use aic_sdk_sys::{AicVadParameter::*, *};
use std::{ffi::CString, marker::PhantomData, ptr};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VadParameter {
SpeechHoldDuration,
Sensitivity,
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,
}
}
}
pub struct Vad<'a> {
inner: *mut AicVad,
initialized: bool,
marker: PhantomData<&'a [u8]>,
}
impl<'a> Vad<'a> {
pub fn new(model: &Model<'a>, license_key: &str) -> Result<Self, AicError> {
Self::create(model, license_key, None)
}
pub fn with_otel_config(
model: &Model<'a>,
license_key: &str,
otel_config: &OtelConfig,
) -> Result<Self, AicError> {
Self::create(model, license_key, Some(otel_config))
}
fn create(
model: &Model<'a>,
license_key: &str,
otel_config: Option<&OtelConfig>,
) -> Result<Self, AicError> {
unsafe { crate::set_sdk_id(2) };
let c_session_id = otel_config
.and_then(|o| o.session_id.as_deref())
.map(CString::new)
.transpose()
.map_err(|_| AicError::Internal)?;
let c_otel = otel_config.map(|o| AicOtelConfig {
enable: o.enable,
session_id: c_session_id.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
export_interval_ms: o.export_interval_ms,
});
let c_otel_ptr = c_otel
.as_ref()
.map_or(ptr::null(), |o| o as *const AicOtelConfig);
let mut vad_ptr: *mut AicVad = ptr::null_mut();
let c_license_key =
CString::new(license_key).map_err(|_| AicError::LicenseFormatInvalid)?;
let error_code = unsafe {
aic_vad_create(
&mut vad_ptr,
model.as_const_ptr(),
c_license_key.as_ptr(),
c_otel_ptr,
)
};
handle_error(error_code)?;
assert!(
!vad_ptr.is_null(),
"C library returned success but null pointer"
);
Ok(Self {
inner: vad_ptr,
initialized: false,
marker: PhantomData,
})
}
pub fn with_config(mut self, config: &ProcessorConfig) -> Result<Self, AicError> {
self.initialize(config)?;
Ok(self)
}
pub fn initialize(&mut self, config: &ProcessorConfig) -> Result<(), AicError> {
let error_code = unsafe {
aic_vad_initialize(
self.inner,
config.sample_rate,
config.block_size,
config.variable_block_size,
)
};
handle_error(error_code)?;
self.initialized = true;
Ok(())
}
pub fn process(&mut self, audio: &[f32]) -> Result<(), AicError> {
if !self.initialized {
return Err(AicError::NotInitialized);
}
let audio_len = audio.len();
let error_code = unsafe { aic_vad_process(self.inner, audio.as_ptr(), audio_len) };
handle_error(error_code)
}
pub fn context(&self) -> VadContext {
let mut context_ptr: *mut AicVadContext = ptr::null_mut();
let error_code = unsafe { aic_vad_context_create(&mut context_ptr, self.as_const_ptr()) };
assert!(handle_error(error_code).is_ok());
assert!(
!context_ptr.is_null(),
"C library returned success but null pointer"
);
VadContext::new(context_ptr)
}
pub fn terminate_session(&mut self) -> Result<(), AicError> {
let error_code = unsafe { aic_vad_terminate_session(self.inner) };
handle_error(error_code)
}
fn as_const_ptr(&self) -> *const AicVad {
self.inner as *const AicVad
}
}
impl<'a> Drop for Vad<'a> {
fn drop(&mut self) {
if !self.inner.is_null() {
unsafe { aic_vad_destroy(self.inner) };
}
}
}
unsafe impl<'a> Send for Vad<'a> {}
unsafe impl<'a> Sync for Vad<'a> {}
pub struct VadContext {
inner: *mut AicVadContext,
}
impl VadContext {
pub(crate) fn new(context_ptr: *mut AicVadContext) -> Self {
Self { inner: context_ptr }
}
fn as_const_ptr(&self) -> *const AicVadContext {
self.inner as *const AicVadContext
}
pub fn is_speech_detected(&self) -> bool {
let mut value: bool = false;
let error_code =
unsafe { aic_vad_context_is_speech_detected(self.as_const_ptr(), &mut value) };
assert!(handle_error(error_code).is_ok());
value
}
pub fn raw_vad_probability(&self) -> f32 {
let mut value: f32 = 0.0;
let error_code =
unsafe { aic_vad_context_get_raw_vad_probability(self.as_const_ptr(), &mut value) };
assert!(handle_error(error_code).is_ok());
value
}
pub fn set_parameter(&self, parameter: VadParameter, value: f32) -> Result<(), AicError> {
let error_code =
unsafe { aic_vad_context_set_parameter(self.as_const_ptr(), parameter.into(), value) };
handle_error(error_code)
}
pub fn parameter(&self, parameter: VadParameter) -> Result<f32, AicError> {
let mut value: f32 = 0.0;
let error_code = unsafe {
aic_vad_context_get_parameter(self.as_const_ptr(), parameter.into(), &mut value)
};
handle_error(error_code)?;
Ok(value)
}
pub fn prediction_delay(&self) -> usize {
let mut delay: usize = 0;
let error_code =
unsafe { aic_vad_context_get_prediction_delay(self.as_const_ptr(), &mut delay) };
assert_success(
error_code,
"`aic_vad_context_get_prediction_delay` failed. This is a bug, please open an issue on GitHub for further investigation.",
);
delay
}
pub fn reset(&self) -> Result<(), AicError> {
let error_code = unsafe { aic_vad_context_reset(self.as_const_ptr()) };
handle_error(error_code)
}
pub fn update_bearer_token(&self, token: &str) -> Result<(), AicError> {
let c_token = CString::new(token).map_err(|_| AicError::LicenseFormatInvalid)?;
let error_code =
unsafe { aic_vad_context_update_bearer_token(self.as_const_ptr(), c_token.as_ptr()) };
handle_error(error_code)
}
}
impl Drop for VadContext {
fn drop(&mut self) {
if !self.inner.is_null() {
unsafe { aic_vad_context_destroy(self.inner) };
}
}
}
unsafe impl Send for VadContext {}
unsafe impl Sync for VadContext {}
#[cfg(test)]
mod tests {
use super::*;
use std::{
fs,
path::{Path, PathBuf},
sync::{Mutex, OnceLock},
};
fn download_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
fn find_existing_model(target_dir: &Path, name_fragment: &str) -> Option<PathBuf> {
let entries = fs::read_dir(target_dir).ok()?;
for entry in entries.flatten() {
let path = entry.path();
if path
.file_name()
.and_then(|n| n.to_str())
.map(|name| name.contains(name_fragment) && name.ends_with(".aicmodel"))
.unwrap_or(false)
&& path.is_file()
{
return Some(path);
}
}
None
}
fn get_model(model_id: &str, name_fragment: &str) -> Result<PathBuf, AicError> {
let target_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target");
if let Some(existing) = find_existing_model(&target_dir, name_fragment) {
return Ok(existing);
}
let _guard = download_lock().lock().unwrap();
if let Some(existing) = find_existing_model(&target_dir, name_fragment) {
return Ok(existing);
}
if cfg!(feature = "download-model") {
Model::download(model_id, target_dir)
} else {
panic!(
"Model `{model_id}` not found in {} and `download-model` feature is disabled",
target_dir.display()
);
}
}
fn license_key() -> String {
std::env::var("AIC_SDK_LICENSE")
.expect("AIC_SDK_LICENSE environment variable must be set for tests")
}
fn load_vad_model() -> Model<'static> {
let model_path = get_model("vad-2.1-xxs-16khz", "vad_2_1_xxs_16khz").unwrap();
Model::from_file(&model_path).unwrap()
}
#[test]
fn vad_processes_audio_and_reports_prediction() {
let model = load_vad_model();
let config = ProcessorConfig::optimal(&model);
let mut vad = Vad::new(&model, &license_key())
.unwrap()
.with_config(&config)
.unwrap();
let vad_ctx = vad.context();
assert!(vad_ctx.prediction_delay() > 0);
let audio = vec![0.0f32; config.block_size];
vad.process(&audio).unwrap();
assert!(!vad_ctx.is_speech_detected());
assert!((0.0..=1.0).contains(&vad_ctx.raw_vad_probability()));
vad_ctx.reset().unwrap();
}
#[test]
fn vad_rejects_process_before_initialize() {
let model = load_vad_model();
let mut vad = Vad::new(&model, &license_key()).unwrap();
let audio = vec![0.0f32; 160];
assert_eq!(vad.process(&audio), Err(AicError::NotInitialized));
}
#[test]
fn vad_rejects_enhancement_model() {
let model_path = get_model("rook-s-48khz", "rook_s_48khz").unwrap();
let model = Model::from_file(&model_path).unwrap();
assert_eq!(
Vad::new(&model, &license_key()).err(),
Some(AicError::ModelTypeUnsupported)
);
}
#[test]
fn vad_parameters_round_trip() {
let model = load_vad_model();
let vad = Vad::new(&model, &license_key()).unwrap();
let vad_ctx = vad.context();
vad_ctx
.set_parameter(VadParameter::Sensitivity, 0.5)
.unwrap();
assert_eq!(vad_ctx.parameter(VadParameter::Sensitivity).unwrap(), 0.5);
assert_eq!(
vad_ctx.set_parameter(VadParameter::Sensitivity, 7.0),
Err(AicError::ParameterOutOfRange)
);
}
#[test]
fn vad_is_send_and_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Vad>();
assert_sync::<Vad>();
assert_send::<VadContext>();
assert_sync::<VadContext>();
}
}
#[doc(hidden)]
mod _compile_fail_tests {
}