Files
codex/codex-rs/voice-host/src/main_tests.rs
T
Benjamin Carlsson 7dc7c7a756 Send processed microphone audio over RTP in voice-host (#43090)
## Why

`voice-host` previously drained captured audio locally without sending it to the peer. Connect capture to the outgoing media track while preserving mute boundaries and limiting stale audio.

## What changed

- Resample capture and render references to 48 kHz with Rubato, apply Sonora echo cancellation, noise suppression, and gain control, and encode capture as 20 ms mono Opus packets.
- Attach an Opus track to the WebRTC peer and advance RTP timestamps across mute gaps without introducing sequence-number gaps.
- Prioritize queued controls and shutdown between sends. Clear pending audio and processing history on microphone mute transitions, reject delayed pre-unmute buffers, and reset partial history after capture gaps.
- Size bounded capture and render queues for device rates, validate callback timing, enforce a 500 ms processing deadline, and time out stalled sends after 100 ms.

Receive/decode and TUI integration remain unimplemented. The bundled Opus encoder build requires CMake and a C compiler.

## Testing

Add synthetic-audio tests for Opus decoding at 44.1 and 48 kHz input rates, echo reduction, capture gaps, stale audio, queue capacity, and control priority. A loopback WebRTC test verifies RTP delivery, decoded signal, mute isolation, and timestamp and sequence continuity across mute gaps.

GitOrigin-RevId: 38b8438577cfadb8124f8aeb426dffbab4845964
2026-09-05 20:02:55 +00:00

130 lines
4.0 KiB
Rust

//! Verify control-first capture servicing and the real parent-loss watchdog during blocked startup.
use super::*;
use std::process::Stdio;
use pretty_assertions::assert_eq;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use tokio::time::timeout;
const STARTUP_BLOCKED: &[u8] = b"voice startup blocked\n";
const CHILD_ENV: &str = "CODEX_VOICE_WATCHDOG_TEST_CHILD";
#[test]
fn queued_privacy_controls_and_close_precede_capture_service() {
for message in [
Message::SetAudioControls {
controls: codex_realtime_webrtc::AudioControls {
microphone_muted: true,
speaker_suppressed: true,
},
},
Message::Close {},
] {
let expected = encode_frame(&message).unwrap();
let (sender, receiver) = mpsc::sync_channel(/*bound*/ 1);
sender.send(message).unwrap();
let next = wait_for_control(&receiver, || panic!("capture preceded queued control"))
.unwrap()
.unwrap();
assert_eq!(encode_frame(&next).unwrap(), expected);
drop(sender);
assert!(
wait_for_control(&receiver, || panic!("capture after parent loss"))
.unwrap()
.is_none()
);
}
}
#[test]
fn idle_service_rechecks_controls_before_another_batch() {
let (sender, receiver) = mpsc::sync_channel(/*bound*/ 1);
let mut services = 0;
let next = wait_for_control(&receiver, || {
services += 1;
sender.send(Message::Close {}).unwrap();
Ok(())
})
.unwrap()
.unwrap();
assert_eq!(next, Message::Close {});
assert_eq!(services, 1);
}
#[test]
#[ignore = "subprocess fixture for the parent-loss watchdog test"]
fn blocked_startup_fixture() {
if std::env::var_os(CHILD_ENV).as_deref() != Some(std::ffi::OsStr::new("1")) {
return;
}
run(|_| {
io::stderr().write_all(STARTUP_BLOCKED)?;
io::stderr().flush()?;
loop {
std::thread::park();
}
})
.unwrap();
}
#[tokio::test]
async fn parent_pipe_loss_terminates_helper_with_blocked_transport_startup() {
timeout(Duration::from_secs(/*secs*/ 10), async {
let mut child = Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"tests::blocked_startup_fixture",
"--ignored",
"--nocapture",
])
.env(CHILD_ENV, "1")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.unwrap();
let mut input = child.stdin.take().unwrap();
input
.write_all(
&encode_frame(&Message::Hello {
protocol: 1,
build_commit: BUILD_COMMIT.to_owned(),
})
.unwrap(),
)
.await
.unwrap();
let expected = encode_frame(&Message::Ready {}).unwrap();
let mut prefix = Vec::new();
// libtest writes a short harness prefix before the helper's framed output.
while !prefix.ends_with(&expected) {
assert!(prefix.len() < 512, "missing helper ready frame");
prefix.push(child.stdout.as_mut().unwrap().read_u8().await.unwrap());
}
input
.write_all(&encode_frame(&Message::StartTransport {}).unwrap())
.await
.unwrap();
let mut marker = vec![0; STARTUP_BLOCKED.len()];
child
.stderr
.as_mut()
.unwrap()
.read_exact(&mut marker)
.await
.unwrap();
assert_eq!(marker, STARTUP_BLOCKED);
assert!(child.try_wait().unwrap().is_none());
drop(input);
let output = child.wait_with_output().await.unwrap();
assert_eq!(output.status.code(), Some(1));
assert_eq!((output.stdout, output.stderr), (vec![], vec![]));
})
.await
.unwrap();
}