mirror of
https://github.com/openai/codex.git
synced 2026-10-24 18:18:41 +00:00
## Why The voice host drained incoming RTP packets without playing them. Route received audio through decoding and speaker output while preserving speaker suppression boundaries. ## What changed - Add a GStreamer pipeline with RTP jitter buffering, Opus decoding, conversion, and resampling into the native speaker sink. - Rebuild playout when the speaker is re-enabled, reject packets received before the new pipeline started, and discard queued audio and echo references on speaker transitions while preserving capture history. - Limit each receive pass to 64 packets so incoming media cannot indefinitely delay audio controls. - Prevent retired sink failures from failing a new speaker generation, and retain the last coherent playback delay estimate during callback contention. ## Testing Add tests for real Opus playback and stale packet rejection, combined microphone mute and speaker suppression, retired sink failures, delay contention, and echo-reference reset without losing capture history. GitOrigin-RevId: c91c94a2ba2347ac98af4e7fd1dc331ae766cc45
139 lines
5.0 KiB
Rust
139 lines
5.0 KiB
Rust
use super::*;
|
|
use crate::devices::buffers::Playback;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
fn active(rate: u32) -> (Arc<Buffers>, PlaybackPort, PlaybackWriter) {
|
|
let buffers = Arc::new(Buffers::new(rate, rate));
|
|
Buffers::set_disabled(&buffers.speaker, /*disabled*/ false).unwrap();
|
|
let port = PlaybackPort::new(buffers.clone(), rate);
|
|
let writer = port.writer();
|
|
(buffers, port, writer)
|
|
}
|
|
|
|
#[test]
|
|
fn partial_writes_account_for_samples_until_the_device_consumes_them() {
|
|
let (buffers, _, writer) = active(/*rate*/ 48000);
|
|
let samples: Vec<_> = (0..BLOCK + 20).map(|i| i as f32 / 512.0).collect();
|
|
let bytes: Vec<_> = samples.iter().flat_map(|v| v.to_le_bytes()).collect();
|
|
let first = writer.write(&bytes).unwrap();
|
|
assert_eq!(first, BLOCK * 4);
|
|
assert_eq!(writer.write(&bytes[first..]).unwrap(), 20 * 4);
|
|
assert_eq!(
|
|
(writer.rate(), writer.delay()),
|
|
(48000, (BLOCK + 20) as u32)
|
|
);
|
|
let mut playback = Playback::default();
|
|
let actual: Vec<_> = (0..samples.len())
|
|
.map(|_| playback.next(&buffers).unwrap())
|
|
.collect();
|
|
assert_eq!(actual, samples);
|
|
assert_eq!(writer.delay(), 0);
|
|
assert_eq!(playback.next(&buffers), None);
|
|
}
|
|
|
|
#[test]
|
|
fn suppression_cancels_a_full_writer_and_old_writers_cannot_resume() {
|
|
let (buffers, port, writer) = active(/*rate*/ 8000);
|
|
let bytes = vec![0; BLOCK * 4];
|
|
writer.write(&bytes).unwrap();
|
|
let waiting = std::thread::spawn(move || writer.write(&bytes));
|
|
Buffers::set_disabled(&buffers.speaker, /*disabled*/ true).unwrap();
|
|
assert_eq!(waiting.join().unwrap(), Err("speaker writer cancelled"));
|
|
let stale = port.writer();
|
|
Buffers::set_disabled(&buffers.speaker, /*disabled*/ false).unwrap();
|
|
assert_eq!(stale.write(&[0; 4]), Err("speaker writer cancelled"));
|
|
let mut playback = Playback::default();
|
|
assert_eq!(playback.next(&buffers), None);
|
|
assert_eq!(buffers.queued.load(Ordering::Acquire), 0);
|
|
assert_eq!(port.writer().write(&[0; 4]), Ok(4));
|
|
}
|
|
|
|
#[test]
|
|
fn delay_includes_pending_dac_time_but_not_another_generation() {
|
|
let (buffers, _, writer) = active(/*rate*/ 48000);
|
|
writer.write(&[0; 40]).unwrap();
|
|
let end = buffers.clock.elapsed() + Duration::from_millis(/*millis*/ 100);
|
|
buffers
|
|
.last_dac_ns
|
|
.store(end.as_nanos() as u64, Ordering::Release);
|
|
let delay = writer.delay();
|
|
assert!((10..=4810).contains(&delay));
|
|
assert!(delay > 10);
|
|
Buffers::set_disabled(&buffers.speaker, /*disabled*/ true).unwrap();
|
|
assert_eq!(writer.delay(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn retired_sink_failure_cannot_fail_the_next_speaker_epoch() {
|
|
let (buffers, port, writer) = active(/*rate*/ 48_000);
|
|
let transition = buffers.speaker_failure_gate.lock().unwrap();
|
|
let (started, ready) = std::sync::mpsc::sync_channel(/*bound*/ 0);
|
|
let failing = std::thread::spawn(move || {
|
|
started.send(()).unwrap();
|
|
writer.fail_if_current();
|
|
});
|
|
ready.recv().unwrap();
|
|
// Hold the same transition gate while retiring the old writer, then let
|
|
// the pending native failure inspect its now-stale epoch.
|
|
Buffers::set_disabled(&buffers.speaker, /*disabled*/ true).unwrap();
|
|
drop(transition);
|
|
failing.join().unwrap();
|
|
assert!(!buffers.failed.load(Ordering::Acquire));
|
|
|
|
buffers.set_speaker_disabled(/*disabled*/ false).unwrap();
|
|
port.writer().fail_if_current();
|
|
assert!(buffers.failed.load(Ordering::Acquire));
|
|
}
|
|
|
|
#[test]
|
|
fn delay_contention_preserves_the_last_snapshot_without_failing_the_device() {
|
|
let (buffers, port, writer) = active(/*rate*/ 48000);
|
|
writer.write(&[0; 40]).unwrap();
|
|
assert_eq!(writer.delay(), 10);
|
|
buffers
|
|
.callback_sequence
|
|
.store(/*val*/ 1, Ordering::Release);
|
|
let mut playback = Playback::default();
|
|
let consumed: Vec<_> = (0..10).map(|_| playback.next(&buffers).unwrap()).collect();
|
|
assert_eq!(consumed, vec![0.0; 10]);
|
|
assert_eq!(
|
|
(writer.delay(), buffers.failed.load(Ordering::Acquire)),
|
|
(10, false)
|
|
);
|
|
buffers
|
|
.callback_sequence
|
|
.store(/*val*/ 2, Ordering::Release);
|
|
assert_eq!(writer.delay(), 0);
|
|
|
|
writer.write(&[0; 20]).unwrap();
|
|
assert_eq!(writer.delay(), 5);
|
|
buffers
|
|
.callback_sequence
|
|
.store(/*val*/ 3, Ordering::Release);
|
|
Buffers::set_disabled(&buffers.speaker, /*disabled*/ true).unwrap();
|
|
assert_eq!(writer.delay(), 0);
|
|
Buffers::set_disabled(&buffers.speaker, /*disabled*/ false).unwrap();
|
|
assert_eq!(
|
|
(
|
|
port.writer().delay(),
|
|
buffers.failed.load(Ordering::Acquire)
|
|
),
|
|
(0, false)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_samples_and_stalled_consumption_fail_without_growing_the_queue() {
|
|
let (buffers, _, writer) = active(/*rate*/ 8000);
|
|
assert_eq!(
|
|
writer.write(&f32::NAN.to_le_bytes()),
|
|
Err("invalid speaker sample")
|
|
);
|
|
writer.write(&vec![0; BLOCK * 4]).unwrap();
|
|
assert_eq!(
|
|
writer.write(&vec![0; BLOCK * 4]),
|
|
Err("speaker fell behind")
|
|
);
|
|
assert_eq!(buffers.queued.load(Ordering::Acquire), BLOCK as u32);
|
|
}
|