mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
## Why A second audio ring can overwrite late speech with silence, and valid RTP bursts can exceed the jitter buffer's 60 ms reordering window and be dropped. ## What changed - Forward decoded PCM directly from a GStreamer `BaseSink` to the bounded CPAL queue, letting CPAL pace playback and handling partial writes. - Disable `drop-on-latency` so valid queued speech survives packet bursts; existing ingress permits continue to bound retained packets and bytes. - Cancel blocked writes on native flushes while preserving speaker epoch isolation, and remove the obsolete playback delay accounting. ## Testing Add regression tests for preserving PCM across a pause, forwarding all admitted packets in an RTP burst, and cancelling a blocked speaker write on an unexpected flush. GitOrigin-RevId: c299c6289563d6b4d943362bc3463dbfce9289e7
98 lines
3.8 KiB
Rust
98 lines
3.8 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(), buffers.queued.load(Ordering::Acquire)),
|
|
(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!(buffers.queued.load(Ordering::Acquire), 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];
|
|
for _ in 0..if cfg!(target_os = "linux") { 3 } else { 1 } {
|
|
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 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 invalid_samples_fail_but_stalled_consumption_drops_and_resumes() {
|
|
let (buffers, _, writer) = active(/*rate*/ 8000);
|
|
assert_eq!(
|
|
writer.write(&f32::NAN.to_le_bytes()),
|
|
Err("invalid speaker sample")
|
|
);
|
|
let blocks = if cfg!(target_os = "linux") { 3 } else { 1 };
|
|
for _ in 0..blocks {
|
|
writer.write(&vec![0; BLOCK * 4]).unwrap();
|
|
}
|
|
assert_eq!(writer.write(&vec![0; BLOCK * 4]), Ok(BLOCK * 4));
|
|
assert_eq!(
|
|
buffers.queued.load(Ordering::Acquire),
|
|
(blocks * BLOCK) as u32
|
|
);
|
|
let mut playback = Playback::default();
|
|
for _ in 0..blocks * BLOCK {
|
|
assert!(playback.next(&buffers).is_some());
|
|
}
|
|
assert_eq!(writer.write(&vec![0; BLOCK * 4]), Ok(BLOCK * 4));
|
|
assert!(!buffers.failed.load(Ordering::Acquire));
|
|
}
|