use std::collections::VecDeque; use std::sync::Arc;
use anyhow::Result; use audiopus::coder::Encoder as OpusEncoder; use audiopus::{Application, Channels, SampleRate}; use parking_lot::Mutex; use tracing::{error, info, warn};
use crate::ipc::{send_msg, OutMsg};
struct GainEnvelope { current: f32, target: f32, step_per_sample: f32, }
impl GainEnvelope { fn new(current: f32, target: f32, fade_ms: u32) -> Self { let total_samples = 48_000.0 * fade_ms as f64 / 1000.0; let step = if total_samples > 0.0 { (target as f64 - current as f64) / total_samples } else { 0.0 }; Self { current, target, step_per_sample: step as f32, } }
fn apply_sample(&mut self, sample: i16) -> i16 {
let out = (sample as f32 * self.current).clamp(-32768.0, 32767.0) as i16;
self.advance();
out
}
fn advance(&mut self) {
if (self.step_per_sample > 0.0 && self.current < self.target)
|| (self.step_per_sample < 0.0 && self.current > self.target)
{
self.current += self.step_per_sample;
if (self.step_per_sample > 0.0 && self.current > self.target)
|| (self.step_per_sample < 0.0 && self.current < self.target)
{
self.current = self.target;
}
}
}
fn is_complete(&self) -> bool {
(self.current - self.target).abs() < 0.0001
}
}
const MAX_TRAILING_SILENCE: u32 = 5; // 100ms of trailing silence pub(crate) const AUDIO_FRAME_SAMPLES: usize = 960; // 20ms @ 48kHz mono const MAX_PCM_BUFFER_SAMPLES: usize = 720_000; // 15 seconds @ 48kHz mono pub(crate) const MAX_MUSIC_BUFFER_SAMPLES: usize = 96_000; // 2 seconds @ 48kHz mono const PARTIAL_TTS_FLUSH_TICKS: u32 = 2; // Flush an underfilled tail after 40ms of no growth
/// Minimum TTS samples to accumulate before starting playback after the buffer /// was empty. This absorbs burst-latency gaps from streaming TTS providers /// (e.g. ElevenLabs) at the cost of a small initial delay (~200ms). Once the /// pre-buffer is satisfied, playback runs continuously until the buffer drains. const TTS_PREBUFFER_SAMPLES: usize = 9_600; // 200ms @ 48kHz mono
/// Maximum ticks to wait for the pre-buffer to fill before releasing the gate. /// If no new samples arrive for this many consecutive ticks, the utterance is /// shorter than TTS_PREBUFFER_SAMPLES and we should play it immediately rather /// than holding it forever. 3 ticks = 60ms of no growth. const TTS_PREBUFFER_STALL_TICKS: u32 = 3;
pub(crate) struct AudioSendState { pcm_buffer: VecDeque, // TTS audio music_buffer: VecDeque, // Music audio (separate for mixing) music_gain: GainEnvelope, // Gain envelope applied to music music_gain_notified: f32, // Last gain value we sent MusicGainReached for music_output_suppressed: bool, encoder: OpusEncoder, speaking: bool, trailing_silence_frames: u32, partial_tts_stall_ticks: u32, /// When true, TTS playback has started and the pre-buffer threshold no /// longer applies until the buffer fully drains back to zero. tts_prebuffer_satisfied: bool, /// Tracks how many consecutive ticks the TTS buffer has been below /// TTS_PREBUFFER_SAMPLES without growing. When this reaches /// TTS_PREBUFFER_STALL_TICKS the gate releases — the utterance is /// shorter than the threshold and no more data is coming. tts_prebuffer_stall_ticks: u32, /// Snapshot of the TTS buffer size on the previous prebuffer-gate tick, /// used to detect whether the buffer is still growing. tts_prebuffer_last_len: usize, }
impl AudioSendState { pub(crate) fn new() -> Result { let encoder = OpusEncoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip) .map_err(|e| anyhow::anyhow!("Opus encoder init: {e:?}"))?; // Pre-allocate for ~1 second of audio at 48kHz mono. This avoids // repeated small allocations during the initial buffering phase. Ok(Self { pcm_buffer: VecDeque::with_capacity(48_000), music_buffer: VecDeque::with_capacity(48_000), music_gain: GainEnvelope::new(1.0, 1.0, 0), music_gain_notified: 1.0, music_output_suppressed: false, encoder, speaking: false, trailing_silence_frames: 0, partial_tts_stall_ticks: 0, tts_prebuffer_satisfied: false, tts_prebuffer_stall_ticks: 0, tts_prebuffer_last_len: 0, }) }
pub(crate) fn push_pcm(&mut self, samples: Vec<i16>) {
self.pcm_buffer.extend(samples);
// Drop newest samples to keep the buffer bounded without skipping ahead
// through speech that is already queued for playback.
if self.pcm_buffer.len() > MAX_PCM_BUFFER_SAMPLES {
let overflow = self.pcm_buffer.len() - MAX_PCM_BUFFER_SAMPLES;
warn!(
"TTS PCM buffer overflow: dropping {} newest samples ({:.1}ms), buffer was {} samples ({:.1}ms)",
overflow,
overflow as f64 / 48.0,
self.pcm_buffer.len(),
self.pcm_buffer.len() as f64 / 48.0
);
self.pcm_buffer.truncate(MAX_PCM_BUFFER_SAMPLES);
}
self.trailing_silence_frames = 0;
self.partial_tts_stall_ticks = 0;
}
pub(crate) fn push_music_pcm(&mut self, samples: Vec<i16>) {
self.music_buffer.extend(samples);
self.trailing_silence_frames = 0;
}
pub(crate) fn can_accept_music_chunk(&self) -> bool {
self.music_buffer.len().saturating_add(AUDIO_FRAME_SAMPLES) <= MAX_MUSIC_BUFFER_SAMPLES
}
pub(crate) fn suppress_music_output(&mut self) {
self.music_output_suppressed = true;
}
pub(crate) fn resume_music_output(&mut self) {
self.music_output_suppressed = false;
self.trailing_silence_frames = 0;
}
pub(crate) fn set_music_gain(&mut self, target: f32, fade_ms: u32) -> Option<f32> {
let current = if fade_ms == 0 {
target
} else {
self.music_gain.current
};
self.music_gain = GainEnvelope::new(current, target, fade_ms);
if fade_ms == 0 {
self.music_gain_notified = target;
Some(target)
} else {
None
}
}
pub(crate) fn begin_music_fade_in(&mut self, fade_ms: u32) {
self.music_gain = GainEnvelope::new(0.0, 1.0, fade_ms);
self.music_gain_notified = 0.0;
}
pub(crate) fn maybe_take_music_gain_reached(&mut self) -> Option<f32> {
if self.music_gain.is_complete()
&& (self.music_gain_notified - self.music_gain.target).abs() > 0.0001
{
let reached = self.music_gain.target;
self.music_gain_notified = reached;
Some(reached)
} else {
None
}
}
pub(crate) fn is_music_fade_out_complete(&self) -> bool {
self.music_gain.is_complete() && self.music_gain.target < 0.001
}
pub(crate) fn is_music_ducked(&self) -> bool {
self.music_gain.target < 1.0
}
pub(crate) fn tts_is_empty(&self) -> bool {
self.pcm_buffer.is_empty()
}
pub(crate) fn tts_buffer_samples(&self) -> usize {
self.pcm_buffer.len()
}
pub(crate) fn music_buffer_samples(&self) -> usize {
self.music_buffer.len()
}
#[cfg(test)]
pub(crate) fn is_music_output_suppressed(&self) -> bool {
self.music_output_suppressed
}
fn clear(&mut self) {
self.pcm_buffer.clear();
self.music_buffer.clear();
self.music_output_suppressed = false;
self.trailing_silence_frames = MAX_TRAILING_SILENCE;
self.partial_tts_stall_ticks = 0;
self.tts_prebuffer_satisfied = false;
self.tts_prebuffer_stall_ticks = 0;
self.tts_prebuffer_last_len = 0;
}
fn clear_tts(&mut self) {
self.pcm_buffer.clear();
self.partial_tts_stall_ticks = 0;
self.tts_prebuffer_satisfied = false;
self.tts_prebuffer_stall_ticks = 0;
self.tts_prebuffer_last_len = 0;
}
fn clear_music(&mut self) {
self.music_buffer.clear();
self.music_output_suppressed = false;
self.trailing_silence_frames = MAX_TRAILING_SILENCE;
}
/// Returns true when the TTS buffer has just transitioned from non-empty to
/// empty (the trailing silence frames have all been sent). The caller
/// should emit an immediate drain notification to the TS side so the
/// output-lock state converges without waiting for the periodic report.
pub(crate) fn tts_just_drained(&self) -> bool {
// The buffer is empty, we were speaking, and we have exhausted all
// trailing silence frames — meaning this tick is the first fully-idle
// tick after playback finished.
!self.speaking
&& self.pcm_buffer.is_empty()
&& self.trailing_silence_frames >= MAX_TRAILING_SILENCE
&& !self.tts_prebuffer_satisfied
}
/// Encode the next 20ms frame, mixing TTS and music buffers.
/// Music samples have the gain envelope applied unless music output is
/// temporarily suppressed for a wake-word pause. Returns None if idle.
pub(crate) fn next_opus_frame(&mut self) -> Option<Vec<u8>> {
let available_tts = self.pcm_buffer.len();
let available_music = self.music_buffer.len();
let has_music = !self.music_output_suppressed && available_music >= AUDIO_FRAME_SAMPLES;
// Pre-buffer gate: when transitioning from empty to having TTS data,
// wait until the buffer reaches TTS_PREBUFFER_SAMPLES before starting
// playback. This absorbs TTS streaming burst gaps so the output is
// continuous. Once the threshold is met, play normally until the
// buffer fully drains back to zero.
//
// Safety valve: if the buffer stops growing for TTS_PREBUFFER_STALL_TICKS
// (60ms) without reaching the threshold, the utterance is shorter than
// the pre-buffer target — release the gate and play what we have.
if !self.tts_prebuffer_satisfied
&& available_tts > 0
&& available_tts < TTS_PREBUFFER_SAMPLES
{
if available_tts > self.tts_prebuffer_last_len {
// Buffer is still growing — reset the stall counter.
self.tts_prebuffer_stall_ticks = 0;
} else {
self.tts_prebuffer_stall_ticks = self.tts_prebuffer_stall_ticks.saturating_add(1);
}
self.tts_prebuffer_last_len = available_tts;
if self.tts_prebuffer_stall_ticks < TTS_PREBUFFER_STALL_TICKS && !has_music {
// Still accumulating — don't produce TTS output yet.
self.partial_tts_stall_ticks = 0;
return None;
}
// Stall timeout reached — short utterance, release the gate.
self.tts_prebuffer_satisfied = true;
info!(
buffered_samples = available_tts,
buffered_ms = available_tts as f64 / 48.0,
stall_ticks = self.tts_prebuffer_stall_ticks,
"clankvox_tts_prebuffer_satisfied_short_utterance"
);
}
if available_tts >= TTS_PREBUFFER_SAMPLES && !self.tts_prebuffer_satisfied {
self.tts_prebuffer_satisfied = true;
self.tts_prebuffer_stall_ticks = 0;
self.tts_prebuffer_last_len = 0;
info!(
buffered_samples = available_tts,
buffered_ms = available_tts as f64 / 48.0,
"clankvox_tts_prebuffer_satisfied"
);
}
let has_full_tts = available_tts >= AUDIO_FRAME_SAMPLES;
let has_partial_tts = available_tts > 0 && available_tts < AUDIO_FRAME_SAMPLES;
if has_full_tts {
self.partial_tts_stall_ticks = 0;
} else if has_partial_tts {
self.partial_tts_stall_ticks = self.partial_tts_stall_ticks.saturating_add(1);
} else {
self.partial_tts_stall_ticks = 0;
}
let flush_partial_tts = has_partial_tts
&& (has_music || self.partial_tts_stall_ticks >= PARTIAL_TTS_FLUSH_TICKS);
let tts_samples_to_take = if has_full_tts {
AUDIO_FRAME_SAMPLES
} else if flush_partial_tts {
available_tts
} else {
0
};
let has_tts = tts_samples_to_take > 0;
if has_tts || has_music {
let mut mixed = [0i32; AUDIO_FRAME_SAMPLES];
if has_music {
for (i, s) in self.music_buffer.drain(..AUDIO_FRAME_SAMPLES).enumerate() {
mixed[i] += self.music_gain.apply_sample(s) as i32;
}
}
if has_tts {
if flush_partial_tts && tts_samples_to_take < AUDIO_FRAME_SAMPLES {
info!(
queued_samples = available_tts,
stall_ticks = self.partial_tts_stall_ticks,
"clankvox_tts_partial_frame_flushed"
);
}
for (i, s) in self.pcm_buffer.drain(..tts_samples_to_take).enumerate() {
mixed[i] += s as i32; // TTS at full volume always
}
}
let pcm: Vec<i16> = mixed
.iter()
.map(|&s| s.clamp(-32768, 32767) as i16)
.collect();
let mut opus_buf = vec![0u8; 4000];
match self.encoder.encode(&pcm, &mut opus_buf) {
Ok(len) => {
self.speaking = true;
self.trailing_silence_frames = 0;
self.partial_tts_stall_ticks = 0;
return Some(opus_buf[..len].to_vec());
}
Err(e) => {
error!("Opus encode error: {:?}", e);
return None;
}
}
}
if has_partial_tts {
// Hold a short underfilled tail briefly so adjacent deltas can coalesce
// into a full 20ms frame; otherwise we would pad too aggressively and
// create choppy playback. If the tail does not grow, a later tick will
// flush it as a padded final frame.
return None;
}
// Buffer empty — send trailing silence to avoid abrupt cutoff
if self.trailing_silence_frames < MAX_TRAILING_SILENCE {
self.trailing_silence_frames += 1;
// Opus silence frame (RFC 6716 comfort noise)
return Some(vec![0xF8, 0xFF, 0xFE]);
}
if self.speaking {
self.speaking = false;
// Buffer is fully drained and trailing silence is done — reset the
// pre-buffer gate so the next batch of TTS audio gets the full
// pre-buffer treatment.
self.tts_prebuffer_satisfied = false;
self.tts_prebuffer_stall_ticks = 0;
self.tts_prebuffer_last_len = 0;
}
None
}
}
/// Windowed-sinc low-pass FIR filter + polyphase resampling. /// /// For integer-ratio downsampling (e.g. 48 kHz → 24 kHz) this applies a /// proper anti-aliasing filter before decimation, preventing high-frequency /// content from folding back into the output band. /// /// For non-integer ratios the filter kernel is interpolated at fractional /// positions (polyphase decomposition), which is equivalent to a high-quality /// sinc resampler. /// /// Filter parameters: /// - Kernel half-length: 16 taps per lobe (32-tap symmetric FIR) /// - Window: Blackman (excellent stopband attenuation ≈ −74 dB) /// - Cutoff: 0.45 × min(in_rate, out_rate) to leave transition room pub(crate) fn resample_mono_i16(input: &[i16], in_rate: u32, out_rate: u32) -> Vec { if in_rate == out_rate || input.len() <= 1 { return input.to_vec(); } let ratio = in_rate as f64 / out_rate as f64; let out_len = ((input.len() as f64) / ratio).floor() as usize; if out_len == 0 { return vec![]; }
// Number of zero-crossings on each side of the sinc kernel.
const SINC_HALF_LEN: usize = 16;
// Cutoff relative to the lower of the two rates, with margin for the
// transition band.
let cutoff = 0.45 / ratio.max(1.0);
let mut output = Vec::with_capacity(out_len);
for i in 0..out_len {
let center = i as f64 * ratio;
let i_center = center.floor() as isize;
let mut sum = 0.0f64;
let mut weight_sum = 0.0f64;
let start = (i_center - SINC_HALF_LEN as isize).max(0);
let end = (i_center + SINC_HALF_LEN as isize + 1).min(input.len() as isize);
for j in start..end {
let x = (j as f64 - center) * cutoff * 2.0;
// sinc(x) = sin(πx)/(πx), with sinc(0) = 1
let sinc = if x.abs() < 1e-10 {
1.0
} else {
let px = std::f64::consts::PI * x;
px.sin() / px
};
// Blackman window
let n = j as f64 - center;
let win_pos = (n / SINC_HALF_LEN as f64 + 1.0) * 0.5; // 0..1
let w = 0.42 - 0.5 * (2.0 * std::f64::consts::PI * win_pos).cos()
+ 0.08 * (4.0 * std::f64::consts::PI * win_pos).cos();
let kernel = sinc * w;
sum += input[j as usize] as f64 * kernel;
weight_sum += kernel;
}
let sample = if weight_sum.abs() > 1e-10 {
sum / weight_sum
} else {
0.0
};
output.push(sample.round().clamp(-32768.0, 32767.0) as i16);
}
output
}
/// Convert LLM output (mono i16 LE at in_rate) to 48kHz mono i16 for Opus encoding.
pub(crate) fn convert_llm_to_48k_mono(pcm: &[u8], in_rate: u32) -> Vec {
let sample_count = pcm.len() / 2;
if sample_count == 0 {
return vec![];
}
let mut mono = Vec::with_capacity(sample_count);
for i in 0..sample_count {
mono.push(i16::from_le_bytes([pcm[i * 2], pcm[i * 2 + 1]]));
}
resample_mono_i16(&mono, in_rate, 48000)
}
/// Convert decoded stereo i16 48kHz to LLM input (mono i16 LE at out_rate).
/// Returns (LLM input, signal_peak_abs, signal_active_sample_count, signal_sample_count)
pub(crate) fn convert_decoded_to_llm(
stereo_i16: &[i16],
out_rate: u32,
) -> (Vec, u16, usize, usize) {
let frame_count = stereo_i16.len() / 2;
if frame_count == 0 {
return (vec![], 0, 0, 0);
}
// Stereo-to-mono downmix via averaging. The /2 inherently attenuates by
// 6 dB — this is standard for mono downmix and is accounted for by the
// downstream signal thresholds. Using (L+R+1)>>1 instead of (L+R)/2
// rounds to nearest rather than truncating toward zero, which eliminates
// a small DC bias on low-amplitude signals.
let mut mono = Vec::with_capacity(frame_count);
for i in 0..frame_count {
let l = stereo_i16[i * 2] as i32;
let r = stereo_i16[i * 2 + 1] as i32;
mono.push(((l + r + 1) >> 1).clamp(-32768, 32767) as i16);
}
let resampled = resample_mono_i16(&mono, 48000, out_rate);
let mut buf = Vec::with_capacity(resampled.len() * 2);
let mut max_amp: u16 = 0;
let mut active_samples = 0;
for &s in &resampled {
buf.extend_from_slice(&s.to_le_bytes());
let abs_val = s.unsigned_abs();
if abs_val > max_amp {
max_amp = abs_val;
}
if abs_val > 500 {
active_samples += 1;
}
}
(buf, max_amp, active_samples, resampled.len())
}
pub(crate) fn clear_audio_send_buffer(audio_send_state: &Arc<Mutex<Option>>) { let mut guard = audio_send_state.lock(); if let Some(ref mut state) = *guard { state.clear(); } }
pub(crate) fn clear_tts_send_buffer(audio_send_state: &Arc<Mutex<Option>>) { let mut guard = audio_send_state.lock(); if let Some(ref mut state) = *guard { state.clear_tts(); } }
pub(crate) fn clear_music_send_buffer(audio_send_state: &Arc<Mutex<Option>>) { let mut guard = audio_send_state.lock(); if let Some(ref mut state) = *guard { state.clear_music(); } }
pub(crate) fn suppress_music_output(audio_send_state: &Arc<Mutex<Option>>) { let mut guard = audio_send_state.lock(); if let Some(ref mut state) = *guard { state.suppress_music_output(); } }
pub(crate) fn resume_music_output(audio_send_state: &Arc<Mutex<Option>>) { let mut guard = audio_send_state.lock(); if let Some(ref mut state) = *guard { state.resume_music_output(); } }
pub(crate) fn has_buffered_music_output( audio_send_state: &Arc<Mutex<Option>>, ) -> bool { let guard = audio_send_state.lock(); guard .as_ref() .is_some_and(|state| state.music_buffer_samples() > 0) }
pub(crate) fn emit_playback_armed( reason: &str, audio_send_state: &Arc<Mutex<Option>>, ) { if audio_send_state.lock().is_some() { send_msg(&OutMsg::PlaybackArmed { reason: reason.to_string(), }); } }
#[cfg(test)] mod tests { use super::{AudioSendState, MAX_PCM_BUFFER_SAMPLES};
#[test]
fn tts_partial_tail_flushes_after_short_stall() {
let mut state = AudioSendState::new().expect("audio state");
// Bypass prebuffer gate — this test is about partial-tail flush behavior
state.tts_prebuffer_satisfied = true;
state.push_pcm(vec![123; 480]);
assert_eq!(state.tts_buffer_samples(), 480);
assert!(state.next_opus_frame().is_none());
assert_eq!(state.tts_buffer_samples(), 480);
let frame = state.next_opus_frame().expect("partial tail should flush");
assert!(!frame.is_empty());
assert_eq!(state.tts_buffer_samples(), 0);
}
#[test]
fn tts_partial_tail_coalesces_before_flush_threshold() {
let mut state = AudioSendState::new().expect("audio state");
// Bypass prebuffer gate — this test is about tail coalescing behavior
state.tts_prebuffer_satisfied = true;
state.push_pcm(vec![123; 480]);
assert!(state.next_opus_frame().is_none());
state.push_pcm(vec![123; 480]);
let frame = state
.next_opus_frame()
.expect("full frame should encode once tail grows");
assert!(!frame.is_empty());
assert_eq!(state.tts_buffer_samples(), 0);
}
#[test]
fn tts_prebuffer_gate_holds_output_until_threshold() {
let mut state = AudioSendState::new().expect("audio state");
// Push less than TTS_PREBUFFER_SAMPLES — should be held
state.push_pcm(vec![123; 960]);
assert!(!state.tts_prebuffer_satisfied);
assert!(
state.next_opus_frame().is_none(),
"prebuffer should hold output"
);
assert_eq!(
state.tts_buffer_samples(),
960,
"samples should remain in buffer"
);
// Push enough to cross the threshold
state.push_pcm(vec![123; super::TTS_PREBUFFER_SAMPLES]);
assert!(!state.tts_prebuffer_satisfied);
let frame = state
.next_opus_frame()
.expect("prebuffer satisfied, should produce frame");
assert!(!frame.is_empty());
assert!(state.tts_prebuffer_satisfied);
}
#[test]
fn tts_prebuffer_releases_short_utterance_after_stall() {
let mut state = AudioSendState::new().expect("audio state");
// Push a short utterance (under TTS_PREBUFFER_SAMPLES)
state.push_pcm(vec![123; 960]);
assert!(!state.tts_prebuffer_satisfied);
// Tick 1: first tick sees growth (0 -> 960), resets stall counter
assert!(state.next_opus_frame().is_none());
// Tick 2: no growth, stall_ticks -> 1
assert!(state.next_opus_frame().is_none());
// Tick 3: no growth, stall_ticks -> 2
assert!(state.next_opus_frame().is_none());
// Tick 4: stall_ticks reaches TTS_PREBUFFER_STALL_TICKS (3) -> gate releases
let frame = state
.next_opus_frame()
.expect("short utterance should play after stall timeout");
assert!(!frame.is_empty());
assert!(state.tts_prebuffer_satisfied);
}
#[test]
fn suppressed_music_output_preserves_buffer_until_resumed() {
let mut state = AudioSendState::new().expect("audio state");
state.push_music_pcm(vec![123; 960]);
state.suppress_music_output();
for _ in 0..3 {
let _ = state.next_opus_frame();
}
assert_eq!(state.music_buffer_samples(), 960);
assert!(state.is_music_output_suppressed());
state.resume_music_output();
let frame = state
.next_opus_frame()
.expect("music frame should encode after resume");
assert!(!frame.is_empty());
assert_eq!(state.music_buffer_samples(), 0);
assert!(!state.is_music_output_suppressed());
}
#[test]
fn clear_music_preserves_tts_buffer() {
let mut state = AudioSendState::new().expect("audio state");
state.push_pcm(vec![123; 480]);
state.push_music_pcm(vec![456; 960]);
state.suppress_music_output();
state.clear_music();
assert_eq!(state.tts_buffer_samples(), 480);
assert_eq!(state.music_buffer_samples(), 0);
assert!(!state.is_music_output_suppressed());
}
#[test]
fn tts_overflow_drops_newest_tail_instead_of_skipping_buffered_speech() {
let mut state = AudioSendState::new().expect("audio state");
state.push_pcm(vec![111; MAX_PCM_BUFFER_SAMPLES]);
state.push_pcm(vec![222; 960]);
assert_eq!(state.tts_buffer_samples(), MAX_PCM_BUFFER_SAMPLES);
assert_eq!(state.pcm_buffer.front().copied(), Some(111));
assert_eq!(state.pcm_buffer.back().copied(), Some(111));
}
}
