use std::panic::{AssertUnwindSafe, catch_unwind};
use mediaway_common::{AudioFrame, SampleFormat};
use sonora_agc2::vad_wrapper::VoiceActivityDetectorWrapper;
use sonora_simd::detect_backend;
use crate::apm::error::ApmError;
use crate::apm::pcm::bytes_to_f32;
const SONORA_PCM_SCALE: f32 = 32768.0;
#[derive(Debug)]
pub struct VoiceActivityDetector {
inner: Option<VoiceActivityDetectorWrapper>,
frame_len: usize,
scratch: Vec<f32>,
}
impl VoiceActivityDetector {
pub fn open(sample_rate: u32) -> Result<Self, ApmError> {
let backend = detect_backend();
let rate_i32 = i32::try_from(sample_rate).unwrap_or(i32::MAX);
let build_result = catch_unwind(AssertUnwindSafe(|| {
VoiceActivityDetectorWrapper::new(backend, rate_i32)
}));
let inner = match build_result {
Ok(vad) => Some(vad),
Err(_) => return Err(ApmError::BackendPanicked),
};
let frame_len = usize::try_from(sample_rate / 100).unwrap_or(0);
Ok(Self {
inner,
frame_len,
scratch: vec![0.0; frame_len],
})
}
pub fn analyze(&mut self, frame: &AudioFrame) -> Result<f32, ApmError> {
let Some(inner) = self.inner.as_mut() else {
return Err(ApmError::BackendPanicked);
};
if frame.format != SampleFormat::F32 {
return Err(ApmError::UnsupportedSampleFormat(frame.format));
}
let interleaved = bytes_to_f32(&frame.data);
let channels = usize::from(frame.channels).max(1);
let actual_frames = interleaved.len() / channels;
if actual_frames != self.frame_len {
return Err(ApmError::FrameLengthMismatch {
expected: self.frame_len,
actual: actual_frames,
});
}
let scratch = &mut self.scratch;
for (dst, chunk) in scratch.iter_mut().zip(interleaved.chunks(channels)) {
*dst = chunk[0] * SONORA_PCM_SCALE;
}
let result = catch_unwind(AssertUnwindSafe(|| inner.analyze(scratch)));
if let Ok(probability) = result {
Ok(probability)
} else {
self.inner = None;
Err(ApmError::BackendPanicked)
}
}
#[must_use]
pub const fn is_disabled(&self) -> bool {
self.inner.is_none()
}
}
#[cfg(test)]
#[path = "vad_tests.rs"]
mod tests;