use anyhow::Result;
use crate::kv_cache::InferenceState;
use crate::model::Model;
use crate::model::audio_decoder::{
AudioDecoderWeights, AudioGpu, DepthformerState, DetokenizerState, DetokenizerWeights,
detokenize_to_spectrum, embed_audio_token, istft_to_pcm, sample_audio_frame,
};
use crate::sampler::{Sampler, SamplerConfig};
use crate::time::{Duration, Instant};
use crate::tokenizer::BpeTokenizer;
#[derive(Debug, Clone, PartialEq)]
pub struct AudioGenerateConfig {
pub max_tokens: usize,
pub sampler: SamplerConfig,
pub audio_temperature: f32,
pub audio_top_k: usize,
pub mode: AudioMode,
pub gpu_depthformer: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioMode {
Sequential,
Interleaved,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AudioGenerateResult {
pub text_tokens: usize,
pub audio_frames: usize,
pub audio_samples: usize,
pub elapsed_secs: f64,
pub depthformer_secs: f64,
pub detokenizer_secs: f64,
}
pub const TOKEN_AUDIO_START: u32 = 128;
pub const TOKEN_TEXT_END: u32 = 130;
pub const AUDIO_END_CODE: i32 = 2048;
pub const DEFAULT_INTERLEAVED_TEXT_BUDGET: usize = 6;
pub const DEFAULT_INTERLEAVED_AUDIO_BUDGET: usize = 12;
pub const AUDIO_SAFETY_FRAME_LIMIT: usize = 4096;
#[derive(Debug, Clone, PartialEq)]
pub struct AudioSilenceWatchdog {
pub total_voiced_frames: usize,
pub consecutive_silent_frames: usize,
pub audio_frames_count: usize,
pub rms_threshold: f32,
pub min_voiced_frames: usize,
pub silent_frames_cutoff: usize,
pub silent_frames_cutoff_voiced: usize,
}
impl Default for AudioSilenceWatchdog {
fn default() -> Self {
Self {
total_voiced_frames: 0,
consecutive_silent_frames: 0,
audio_frames_count: 0,
rms_threshold: 0.001,
min_voiced_frames: 5,
silent_frames_cutoff: 30,
silent_frames_cutoff_voiced: 25,
}
}
}
impl AudioSilenceWatchdog {
pub fn new() -> Self {
Self::default()
}
pub fn new_with_thresholds(
rms_threshold: f32,
min_voiced_frames: usize,
silent_frames_cutoff: usize,
silent_frames_cutoff_voiced: usize,
) -> Self {
Self {
total_voiced_frames: 0,
consecutive_silent_frames: 0,
audio_frames_count: 0,
rms_threshold,
min_voiced_frames,
silent_frames_cutoff,
silent_frames_cutoff_voiced,
}
}
#[inline]
pub fn observe_pcm(&mut self, pcm: &[f32]) -> f32 {
self.audio_frames_count += 1;
if pcm.is_empty() {
self.consecutive_silent_frames += 1;
return 0.0;
}
let mut sum = 0.0f32;
let mut valid_samples = 0usize;
for &x in pcm {
if x.is_finite() {
sum += x * x;
valid_samples += 1;
}
}
if valid_samples == 0 {
self.consecutive_silent_frames += 1;
return 0.0;
}
let rms = (sum / valid_samples as f32).sqrt();
if rms >= self.rms_threshold {
self.total_voiced_frames += 1;
self.consecutive_silent_frames = 0;
} else {
self.consecutive_silent_frames += 1;
}
rms
}
#[inline]
pub fn is_silence_terminated(&self) -> bool {
self.consecutive_silent_frames >= self.silent_frames_cutoff
|| (self.total_voiced_frames >= self.min_voiced_frames
&& self.consecutive_silent_frames >= self.silent_frames_cutoff_voiced)
}
#[inline]
pub fn is_safety_limit_reached(&self) -> bool {
self.audio_frames_count >= AUDIO_SAFETY_FRAME_LIMIT
}
}
#[derive(PartialEq)]
enum Modality {
Text,
Audio,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FrameOutcome {
Codes {
audio_embedding: Vec<f32>,
pcm: Vec<f32>,
codes: [i32; 8],
},
End,
}
pub struct AudioOutputDecoder<'a> {
weights: &'a AudioDecoderWeights,
detok_weights: &'a DetokenizerWeights,
gpu: Option<&'a dyn AudioGpu>,
df_state: DepthformerState,
detok_state: DetokenizerState,
streamer: crate::model::audio_decoder::IstftStreamer,
all_spectrum: Vec<f32>,
audio_frames: usize,
streamed_samples: usize,
time_depthformer: Duration,
time_detokenizer: Duration,
audio_temperature: f32,
audio_top_k: usize,
use_gpu_df: bool,
streaming: bool,
all_codes: Vec<i32>,
pub watchdog: AudioSilenceWatchdog,
}
impl<'a> AudioOutputDecoder<'a> {
pub fn audio_frames(&self) -> usize {
self.audio_frames
}
pub fn streamed_samples(&self) -> usize {
self.streamed_samples
}
pub fn sample_rate(&self) -> u32 {
self.detok_weights.config.sample_rate as u32
}
pub fn time_depthformer(&self) -> Duration {
self.time_depthformer
}
pub fn time_detokenizer(&self) -> Duration {
self.time_detokenizer
}
pub fn with_streaming(mut self, streaming: bool) -> Self {
self.streaming = streaming;
self
}
#[inline]
pub fn observe_pcm(&mut self, pcm: &[f32]) -> f32 {
if !self.streaming {
self.watchdog.audio_frames_count += 1;
return 0.0;
}
self.watchdog.observe_pcm(pcm)
}
#[inline]
pub fn is_silence_terminated(&self) -> bool {
self.streaming && self.watchdog.is_silence_terminated()
}
#[inline]
pub fn is_safety_limit_reached(&self) -> bool {
self.watchdog.is_safety_limit_reached()
}
pub fn new(
weights: &'a AudioDecoderWeights,
detok_weights: &'a DetokenizerWeights,
gpu: Option<&'a dyn AudioGpu>,
audio_temperature: f32,
audio_top_k: usize,
gpu_depthformer: bool,
) -> Self {
if let Some(g) = gpu {
g.reset_detokenizer();
g.reset_depthformer();
}
let df_state = DepthformerState::new(&weights.depthformer_config);
let detok_state = DetokenizerState::new(&detok_weights.config);
let streamer = crate::model::audio_decoder::IstftStreamer::new(
detok_weights.config.n_fft,
detok_weights.config.hop_length,
);
Self {
weights,
detok_weights,
gpu,
df_state,
detok_state,
streamer,
all_spectrum: Vec::new(),
audio_frames: 0,
streamed_samples: 0,
time_depthformer: Duration::ZERO,
time_detokenizer: Duration::ZERO,
audio_temperature,
audio_top_k,
use_gpu_df: gpu_depthformer && gpu.is_some_and(|g| g.supports_depthformer()),
streaming: true,
all_codes: Vec::new(),
watchdog: AudioSilenceWatchdog::new(),
}
}
pub fn supports_gpu_depthformer(&self) -> bool {
self.use_gpu_df
}
pub fn decode_frame(&mut self, embed: &[f32]) -> FrameOutcome {
let t0 = Instant::now();
let codes = match (self.use_gpu_df, self.gpu) {
(true, Some(g)) => {
g.sample_audio_frame(embed, self.audio_temperature, self.audio_top_k)
}
_ => sample_audio_frame(
self.weights,
&mut self.df_state,
embed,
self.audio_temperature,
self.audio_top_k,
),
};
self.time_depthformer += t0.elapsed();
if codes[0] == AUDIO_END_CODE {
return FrameOutcome::End;
}
self.audio_frames += 1;
let audio_embedding = embed_audio_token(self.weights, &codes);
if !self.streaming {
self.all_codes.extend_from_slice(&codes);
return FrameOutcome::Codes {
audio_embedding,
pcm: vec![],
codes,
};
}
let t1 = Instant::now();
let spectrum = if let Some(g) = self.gpu {
g.detokenize_to_spectrum(self.detok_weights, &codes)
} else {
detokenize_to_spectrum(
self.detok_weights,
self.weights,
&mut self.detok_state,
&codes,
)
};
self.time_detokenizer += t1.elapsed();
let pcm = self.streamer.feed_frames(&spectrum);
self.streamed_samples += pcm.len();
FrameOutcome::Codes {
audio_embedding,
pcm,
codes,
}
}
pub async fn decode_frame_async(&mut self, embed: &[f32]) -> anyhow::Result<FrameOutcome> {
let t0 = Instant::now();
let codes = match (self.use_gpu_df, self.gpu) {
(true, Some(g)) => {
g.sample_audio_frame_async(embed, self.audio_temperature, self.audio_top_k)
.await?
}
_ => sample_audio_frame(
self.weights,
&mut self.df_state,
embed,
self.audio_temperature,
self.audio_top_k,
),
};
self.time_depthformer += t0.elapsed();
if codes[0] == AUDIO_END_CODE {
return Ok(FrameOutcome::End);
}
self.audio_frames += 1;
let audio_embedding = embed_audio_token(self.weights, &codes);
if !self.streaming {
self.all_codes.extend_from_slice(&codes);
return Ok(FrameOutcome::Codes {
audio_embedding,
pcm: vec![],
codes,
});
}
let t1 = Instant::now();
let spectrum = if let Some(g) = self.gpu {
match g
.detokenize_to_spectrum_async(self.detok_weights, &codes)
.await
{
Ok(spec) => spec,
Err(_) => detokenize_to_spectrum(
self.detok_weights,
self.weights,
&mut self.detok_state,
&codes,
),
}
} else {
detokenize_to_spectrum(
self.detok_weights,
self.weights,
&mut self.detok_state,
&codes,
)
};
self.time_detokenizer += t1.elapsed();
let pcm = self.streamer.feed_frames(&spectrum);
self.streamed_samples += pcm.len();
Ok(FrameOutcome::Codes {
audio_embedding,
pcm,
codes,
})
}
#[cfg(feature = "gpu")]
pub async fn decode_frame_from_gpu_hidden_async(
&mut self,
hidden_buf: &wgpu::Buffer,
) -> anyhow::Result<FrameOutcome> {
let t0 = Instant::now();
let codes = if let (true, Some(g)) = (self.use_gpu_df, self.gpu) {
g.sample_audio_frame_from_gpu_hidden_async(
hidden_buf,
self.audio_temperature,
self.audio_top_k,
)
.await?
} else {
anyhow::bail!("GPU hidden buffer handoff requires GPU depthformer");
};
self.time_depthformer += t0.elapsed();
if codes[0] == AUDIO_END_CODE {
return Ok(FrameOutcome::End);
}
self.audio_frames += 1;
let audio_embedding = embed_audio_token(self.weights, &codes);
if !self.streaming {
self.all_codes.extend_from_slice(&codes);
return Ok(FrameOutcome::Codes {
audio_embedding,
pcm: vec![],
codes,
});
}
let t1 = Instant::now();
let spectrum = if let Some(g) = self.gpu {
match g
.detokenize_to_spectrum_async(self.detok_weights, &codes)
.await
{
Ok(spec) => spec,
Err(_) => detokenize_to_spectrum(
self.detok_weights,
self.weights,
&mut self.detok_state,
&codes,
),
}
} else {
detokenize_to_spectrum(
self.detok_weights,
self.weights,
&mut self.detok_state,
&codes,
)
};
self.time_detokenizer += t1.elapsed();
let pcm = self.streamer.feed_frames(&spectrum);
self.streamed_samples += pcm.len();
Ok(FrameOutcome::Codes {
audio_embedding,
pcm,
codes,
})
}
#[allow(clippy::chunks_exact_to_as_chunks)]
pub fn finish(&mut self, mut sink: impl FnMut(&[f32], u32)) -> usize {
if self.streaming {
let remaining = self.streamer.flush();
if !remaining.is_empty() {
sink(&remaining, self.detok_weights.config.sample_rate as u32);
self.streamed_samples += remaining.len();
}
return self.streamed_samples;
}
if !self.all_codes.is_empty() && self.all_spectrum.is_empty() {
let t1 = Instant::now();
let chunks = self.all_codes.chunks_exact(8);
let remainder = chunks.remainder();
for codes in chunks {
let spectrum = if let Some(g) = self.gpu {
g.detokenize_to_spectrum(self.detok_weights, codes)
} else {
detokenize_to_spectrum(
self.detok_weights,
self.weights,
&mut self.detok_state,
codes,
)
};
self.all_spectrum.extend_from_slice(&spectrum);
}
if !remainder.is_empty() {
tracing::warn!(
"AudioOutputDecoder: discarding {} trailing unaligned audio codes (expected multiple of 8)",
remainder.len()
);
}
self.time_detokenizer += t1.elapsed();
}
if self.all_spectrum.is_empty() {
return self.streamed_samples;
}
let n_fft = self.detok_weights.config.n_fft;
let hop = self.detok_weights.config.hop_length;
let pcm = match self.gpu {
Some(g) => g.istft_to_pcm(&self.all_spectrum, n_fft, hop),
None => istft_to_pcm(&self.all_spectrum, n_fft, hop),
};
self.all_spectrum.clear();
self.all_codes.clear();
if pcm.is_empty() {
return self.streamed_samples;
}
let n = pcm.len();
sink(&pcm, self.detok_weights.config.sample_rate as u32);
self.streamed_samples += n;
self.streamed_samples
}
#[allow(clippy::chunks_exact_to_as_chunks)]
pub async fn finish_async(
&mut self,
mut sink: impl FnMut(&[f32], u32),
) -> anyhow::Result<usize> {
if self.streaming {
let remaining = self.streamer.flush();
if !remaining.is_empty() {
sink(&remaining, self.detok_weights.config.sample_rate as u32);
self.streamed_samples += remaining.len();
}
return Ok(self.streamed_samples);
}
if !self.all_codes.is_empty() && self.all_spectrum.is_empty() {
let t1 = Instant::now();
let chunks = self.all_codes.chunks_exact(8);
let remainder = chunks.remainder();
for codes in chunks {
let spectrum = if let Some(g) = self.gpu {
g.detokenize_to_spectrum_async(self.detok_weights, codes)
.await?
} else {
detokenize_to_spectrum(
self.detok_weights,
self.weights,
&mut self.detok_state,
codes,
)
};
self.all_spectrum.extend_from_slice(&spectrum);
}
if !remainder.is_empty() {
tracing::warn!(
"AudioOutputDecoder: discarding {} trailing unaligned audio codes (expected multiple of 8)",
remainder.len()
);
}
self.time_detokenizer += t1.elapsed();
}
if self.all_spectrum.is_empty() {
return Ok(self.streamed_samples);
}
let n_fft = self.detok_weights.config.n_fft;
let hop = self.detok_weights.config.hop_length;
let pcm = match self.gpu {
Some(g) => g.istft_to_pcm_async(&self.all_spectrum, n_fft, hop).await?,
None => istft_to_pcm(&self.all_spectrum, n_fft, hop),
};
self.all_spectrum.clear();
self.all_codes.clear();
if pcm.is_empty() {
return Ok(self.streamed_samples);
}
let n = pcm.len();
sink(&pcm, self.detok_weights.config.sample_rate as u32);
self.streamed_samples += n;
Ok(self.streamed_samples)
}
}
#[allow(unused_assignments, clippy::too_many_arguments)]
pub fn generate_audio(
model: &dyn Model,
decoder_weights: &AudioDecoderWeights,
detok_weights: &DetokenizerWeights,
tokenizer: &BpeTokenizer,
prompt_tokens: &[u32],
config: &AudioGenerateConfig,
gpu: Option<&dyn AudioGpu>,
mut text_callback: impl FnMut(&str),
mut audio_callback: impl FnMut(&[f32], u32),
) -> Result<AudioGenerateResult> {
anyhow::ensure!(!prompt_tokens.is_empty(), "prompt_tokens must not be empty");
let model_config = model.config();
let mut state = InferenceState::from_config(model_config)?;
let mut sampler = Sampler::new(config.sampler.clone());
let mut decoder = AudioOutputDecoder::new(
decoder_weights,
detok_weights,
gpu,
config.audio_temperature,
config.audio_top_k,
config.gpu_depthformer,
);
let start = Instant::now();
let mut logits = model.forward_prefill(prompt_tokens, 0, &mut state);
let mut modality = Modality::Text;
let mut generated = 0usize;
let mut text_tokens = 0usize;
let mut pos = prompt_tokens.len();
let mut modality_budget = match config.mode {
AudioMode::Interleaved => DEFAULT_INTERLEAVED_TEXT_BUDGET, AudioMode::Sequential => usize::MAX,
};
let mut text_done = false;
let mut next_token = sampler.sample(&mut logits);
'outer: loop {
if generated >= config.max_tokens || pos >= model_config.max_seq_len {
break;
}
if modality == Modality::Text {
if tokenizer.eos_token() == Some(next_token) {
break;
}
if next_token == TOKEN_AUDIO_START {
modality = Modality::Audio;
modality_budget = match config.mode {
AudioMode::Interleaved => DEFAULT_INTERLEAVED_AUDIO_BUDGET,
AudioMode::Sequential => usize::MAX,
};
continue;
}
if next_token == TOKEN_TEXT_END {
text_done = true;
}
if next_token != TOKEN_TEXT_END {
let piece = tokenizer.decode(&[next_token]);
text_callback(&piece);
text_tokens += 1;
}
generated += 1;
modality_budget = modality_budget.saturating_sub(1);
if generated >= config.max_tokens {
break;
}
if matches!(config.mode, AudioMode::Interleaved) && (modality_budget == 0 || text_done)
{
let mut emb = model.forward_embedding(&[next_token], pos, &mut state);
pos += 1;
modality = Modality::Audio;
modality_budget = DEFAULT_INTERLEAVED_AUDIO_BUDGET;
loop {
if pos >= model_config.max_seq_len || decoder.is_safety_limit_reached() {
break;
}
let outcome = decoder.decode_frame(&emb);
let audio_emb = match outcome {
FrameOutcome::End => {
if text_done {
break;
}
logits = model.forward(&[TOKEN_TEXT_END], pos, &mut state);
next_token = sampler.sample(&mut logits);
pos += 1;
break;
}
FrameOutcome::Codes {
audio_embedding,
pcm,
..
} => {
decoder.observe_pcm(&pcm);
if !pcm.is_empty() {
audio_callback(&pcm, decoder.sample_rate());
}
if text_done && decoder.is_silence_terminated() {
break;
}
audio_embedding
}
};
modality_budget = modality_budget.saturating_sub(1);
if generated >= config.max_tokens || pos >= model_config.max_seq_len {
break;
}
if modality_budget == 0 && !text_done {
logits = model.forward_from_embedding(&audio_emb, pos, &mut state);
next_token = sampler.sample(&mut logits);
pos += 1;
break;
}
emb = model.forward_hidden_from_embedding(&audio_emb, pos, &mut state);
pos += 1;
}
if text_done {
break;
}
modality = Modality::Text;
modality_budget = DEFAULT_INTERLEAVED_TEXT_BUDGET;
continue;
}
logits = model.forward(&[next_token], pos, &mut state);
next_token = sampler.sample(&mut logits);
pos += 1;
} else {
let mut emb = model.forward_embedding(&[next_token], pos, &mut state);
pos += 1;
generated += 1;
loop {
if pos >= model_config.max_seq_len || decoder.is_safety_limit_reached() {
break 'outer;
}
let outcome = decoder.decode_frame(&emb);
let audio_emb = match outcome {
FrameOutcome::End => match config.mode {
AudioMode::Sequential => {
break 'outer;
}
AudioMode::Interleaved => {
modality = Modality::Text;
text_done = true;
modality_budget = DEFAULT_INTERLEAVED_TEXT_BUDGET;
logits = model.forward(&[TOKEN_TEXT_END], pos, &mut state);
next_token = sampler.sample(&mut logits);
pos += 1;
break;
}
},
FrameOutcome::Codes {
audio_embedding,
pcm,
..
} => {
decoder.observe_pcm(&pcm);
if !pcm.is_empty() {
audio_callback(&pcm, decoder.sample_rate());
}
if (matches!(config.mode, AudioMode::Sequential) || text_done)
&& decoder.is_silence_terminated()
{
break 'outer;
}
audio_embedding
}
};
modality_budget = modality_budget.saturating_sub(1);
if generated >= config.max_tokens || pos >= model_config.max_seq_len {
break;
}
if matches!(config.mode, AudioMode::Interleaved)
&& modality_budget == 0
&& !text_done
{
modality = Modality::Text;
modality_budget = DEFAULT_INTERLEAVED_TEXT_BUDGET;
logits = model.forward_from_embedding(&audio_emb, pos, &mut state);
next_token = sampler.sample(&mut logits);
pos += 1;
break;
}
emb = model.forward_hidden_from_embedding(&audio_emb, pos, &mut state);
pos += 1;
}
}
}
let audio_samples = decoder.finish(&mut audio_callback);
Ok(AudioGenerateResult {
text_tokens,
audio_frames: decoder.audio_frames,
audio_samples,
elapsed_secs: start.elapsed().as_secs_f64(),
depthformer_secs: decoder.time_depthformer.as_secs_f64(),
detokenizer_secs: decoder.time_detokenizer.as_secs_f64(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audio_silence_watchdog_initial_state() {
let watchdog = AudioSilenceWatchdog::new();
assert_eq!(watchdog.audio_frames_count, 0);
assert_eq!(watchdog.total_voiced_frames, 0);
assert_eq!(watchdog.consecutive_silent_frames, 0);
assert!(!watchdog.is_silence_terminated());
assert!(!watchdog.is_safety_limit_reached());
}
#[test]
fn test_audio_silence_watchdog_unvoiced_cutoff() {
let mut watchdog = AudioSilenceWatchdog::new();
let silent_pcm = vec![0.0f32; 1920];
for _ in 0..29 {
watchdog.observe_pcm(&silent_pcm);
assert!(!watchdog.is_silence_terminated());
}
watchdog.observe_pcm(&silent_pcm);
assert!(watchdog.is_silence_terminated());
}
#[test]
fn test_audio_silence_watchdog_voiced_cutoff() {
let mut watchdog = AudioSilenceWatchdog::new();
let voiced_pcm = vec![0.05f32; 1920];
let silent_pcm = vec![0.0f32; 1920];
for _ in 0..5 {
watchdog.observe_pcm(&voiced_pcm);
}
assert_eq!(watchdog.total_voiced_frames, 5);
assert_eq!(watchdog.consecutive_silent_frames, 0);
assert!(!watchdog.is_silence_terminated());
for _ in 0..24 {
watchdog.observe_pcm(&silent_pcm);
assert!(!watchdog.is_silence_terminated());
}
watchdog.observe_pcm(&silent_pcm);
assert!(watchdog.is_silence_terminated());
}
#[test]
fn test_audio_silence_watchdog_safety_limit() {
let mut watchdog = AudioSilenceWatchdog::new();
watchdog.audio_frames_count = AUDIO_SAFETY_FRAME_LIMIT - 1;
assert!(!watchdog.is_safety_limit_reached());
watchdog.audio_frames_count = AUDIO_SAFETY_FRAME_LIMIT;
assert!(watchdog.is_safety_limit_reached());
}
#[test]
fn test_audio_silence_watchdog_empty_pcm_resilience() {
let mut watchdog = AudioSilenceWatchdog::new();
let empty_pcm: [f32; 0] = [];
let rms = watchdog.observe_pcm(&empty_pcm);
assert_eq!(rms, 0.0);
assert_eq!(watchdog.audio_frames_count, 1);
assert_eq!(watchdog.consecutive_silent_frames, 1);
assert_eq!(watchdog.total_voiced_frames, 0);
}
#[test]
fn test_audio_silence_watchdog_custom_thresholds() {
let mut watchdog = AudioSilenceWatchdog::new_with_thresholds(0.002, 3, 10, 8);
assert_eq!(watchdog.rms_threshold, 0.002);
assert_eq!(watchdog.min_voiced_frames, 3);
assert_eq!(watchdog.silent_frames_cutoff, 10);
assert_eq!(watchdog.silent_frames_cutoff_voiced, 8);
let silent_pcm = vec![0.0f32; 1920];
for _ in 0..9 {
watchdog.observe_pcm(&silent_pcm);
assert!(!watchdog.is_silence_terminated());
}
watchdog.observe_pcm(&silent_pcm);
assert!(watchdog.is_silence_terminated());
}
#[test]
fn test_audio_silence_watchdog_non_finite_resilience() {
let mut watchdog = AudioSilenceWatchdog::new();
let corrupted_pcm = vec![f32::NAN, f32::INFINITY, f32::NEG_INFINITY];
let rms = watchdog.observe_pcm(&corrupted_pcm);
assert_eq!(rms, 0.0);
assert_eq!(watchdog.audio_frames_count, 1);
assert_eq!(watchdog.consecutive_silent_frames, 1);
let mixed_pcm = vec![0.05f32, f32::NAN, 0.05f32];
let rms_mixed = watchdog.observe_pcm(&mixed_pcm);
assert!(rms_mixed > 0.04 && rms_mixed < 0.06);
assert_eq!(watchdog.total_voiced_frames, 1);
assert_eq!(watchdog.consecutive_silent_frames, 0);
}
}