Add realtime audio device config (#12849)

## Summary
- add top-level realtime audio config for microphone and speaker
selection
- apply configured devices when starting realtime capture and playback
- keep missing-device behavior on the system default fallback path

## Validation
- just write-config-schema
- cargo test -p codex-core realtime_audio
- cargo test -p codex-tui
- just fix -p codex-core
- just fix -p codex-tui
- just fmt

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-02-26 15:08:21 -08:00
committed by GitHub
co-authored by Codex
parent fd719d3828
commit a0e86c69fe
7 changed files with 299 additions and 17 deletions
+68
View File
@@ -839,6 +839,30 @@ impl ConfigEditsBuilder {
self
}
pub fn set_realtime_microphone(mut self, microphone: Option<&str>) -> Self {
let segments = vec!["audio".to_string(), "microphone".to_string()];
match microphone {
Some(microphone) => self.edits.push(ConfigEdit::SetPath {
segments,
value: value(microphone),
}),
None => self.edits.push(ConfigEdit::ClearPath { segments }),
}
self
}
pub fn set_realtime_speaker(mut self, speaker: Option<&str>) -> Self {
let segments = vec!["audio".to_string(), "speaker".to_string()];
match speaker {
Some(speaker) => self.edits.push(ConfigEdit::SetPath {
segments,
value: value(speaker),
}),
None => self.edits.push(ConfigEdit::ClearPath { segments }),
}
self
}
pub fn clear_legacy_windows_sandbox_keys(mut self) -> Self {
for key in [
"experimental_windows_sandbox",
@@ -1804,6 +1828,50 @@ model_reasoning_effort = "high"
assert_eq!(notice, Some(true));
}
#[test]
fn blocking_builder_set_realtime_audio_persists_and_clears() {
let tmp = tempdir().expect("tmpdir");
let codex_home = tmp.path();
ConfigEditsBuilder::new(codex_home)
.set_realtime_microphone(Some("USB Mic"))
.set_realtime_speaker(Some("Desk Speakers"))
.apply_blocking()
.expect("persist realtime audio");
let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
let config: TomlValue = toml::from_str(&raw).expect("parse config");
let realtime_audio = config
.get("audio")
.and_then(TomlValue::as_table)
.expect("audio table should exist");
assert_eq!(
realtime_audio.get("microphone").and_then(TomlValue::as_str),
Some("USB Mic")
);
assert_eq!(
realtime_audio.get("speaker").and_then(TomlValue::as_str),
Some("Desk Speakers")
);
ConfigEditsBuilder::new(codex_home)
.set_realtime_microphone(None)
.apply_blocking()
.expect("clear realtime microphone");
let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
let config: TomlValue = toml::from_str(&raw).expect("parse config");
let realtime_audio = config
.get("audio")
.and_then(TomlValue::as_table)
.expect("audio table should exist");
assert_eq!(realtime_audio.get("microphone"), None);
assert_eq!(
realtime_audio.get("speaker").and_then(TomlValue::as_str),
Some("Desk Speakers")
);
}
#[test]
fn replace_mcp_servers_blocking_clears_table_when_empty() {
let tmp = tempdir().expect("tmpdir");