Use aec3 for TUI realtime echo cancellation

Replace the native audio-processing dependency with a smaller pure-Rust realtime echo-cancellation path.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-04-05 14:38:57 -07:00
co-authored by Codex
parent 3e206d1e6a
commit b080c91693
14 changed files with 347 additions and 743 deletions
-107
View File
@@ -278,74 +278,6 @@ http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "ht
http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
new_local_repository = use_repo_rule("@bazel_tools//tools/build_defs/repo:local.bzl", "new_local_repository")
http_archive(
name = "ninja_1.11.0_linux",
urls = [
"https://github.com/ninja-build/ninja/releases/download/v1.11.0/ninja-linux.zip",
],
sha256 = "9726e730d5b8599f82654dc80265e64a10a8a817552c34153361ed0c017f9f02",
strip_prefix = "",
build_file_content = """\
package(default_visibility = ["//visibility:public"])
filegroup(
name = "ninja_bin",
srcs = ["ninja"],
)
""",
)
http_archive(
name = "ninja_1.12.1_linux_aarch64",
urls = [
"https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux-aarch64.zip",
],
sha256 = "5c25c6570b0155e95fce5918cb95f1ad9870df5768653afe128db822301a05a1",
strip_prefix = "",
build_file_content = """\
package(default_visibility = ["//visibility:public"])
filegroup(
name = "ninja_bin",
srcs = ["ninja"],
)
""",
)
http_archive(
name = "ninja_1.11.0_mac",
urls = [
"https://github.com/ninja-build/ninja/releases/download/v1.11.0/ninja-mac.zip",
],
sha256 = "21915277db59756bfc61f6f281c1f5e3897760b63776fd3d360f77dd7364137f",
strip_prefix = "",
build_file_content = """\
package(default_visibility = ["//visibility:public"])
filegroup(
name = "ninja_bin",
srcs = ["ninja"],
)
""",
)
http_archive(
name = "ninja_1.11.0_win",
urls = [
"https://github.com/ninja-build/ninja/releases/download/v1.11.0/ninja-win.zip",
],
sha256 = "d0ee3da143211aa447e750085876c9b9d7bcdd637ab5b2c5b41349c617f22f3b",
strip_prefix = "",
build_file_content = """\
package(default_visibility = ["//visibility:public"])
filegroup(
name = "ninja_bin",
srcs = ["ninja.exe"],
)
""",
)
new_local_repository(
name = "v8_targets",
build_file = "//third_party/v8:BUILD.bazel",
@@ -413,45 +345,6 @@ crate.annotation(
inject_repo(crate, "alsa_lib")
bazel_dep(name = "meson", version = "1.5.1.bcr.1")
crate.annotation(
build_script_tools = [
"@meson//:meson",
"@llvm-project//clang:libclang_interface_output",
"@llvm//:builtin_resource_dir",
"@ninja_1.11.0_linux//:ninja_bin",
"@ninja_1.12.1_linux_aarch64//:ninja_bin",
"@ninja_1.11.0_mac//:ninja_bin",
"@ninja_1.11.0_win//:ninja_bin",
],
build_script_env = {
"BINDGEN_EXTRA_CLANG_ARGS": "-Xclang -internal-isystem -Xclang $(location @llvm//:builtin_resource_dir)/include",
"LIBCLANG_PATH": "$(location @llvm-project//clang:libclang_interface_output)",
"MESON": "$(execpath @meson//:meson)",
},
build_script_env_select = {
"aarch64-apple-darwin": "{\"NINJA_REAL\":\"$(execpath @ninja_1.12.1_linux_aarch64//:ninja_bin)\"}",
"x86_64-apple-darwin": "{\"NINJA_REAL\":\"$(execpath @ninja_1.12.1_linux_aarch64//:ninja_bin)\"}",
"aarch64-unknown-linux-gnu": "{\"NINJA_REAL\":\"$(execpath @ninja_1.11.0_linux//:ninja_bin)\"}",
"x86_64-unknown-linux-gnu": "{\"NINJA_REAL\":\"$(execpath @ninja_1.11.0_linux//:ninja_bin)\"}",
"aarch64-unknown-linux-musl": "{\"NINJA_REAL\":\"$(execpath @ninja_1.11.0_linux//:ninja_bin)\"}",
"x86_64-unknown-linux-musl": "{\"NINJA_REAL\":\"$(execpath @ninja_1.11.0_linux//:ninja_bin)\"}",
"aarch64-pc-windows-msvc": "{\"NINJA\":\"$(execpath @ninja_1.11.0_win//:ninja_bin)\"}",
"x86_64-pc-windows-msvc": "{\"NINJA\":\"$(execpath @ninja_1.11.0_win//:ninja_bin)\"}",
"aarch64-pc-windows-gnullvm": "{\"NINJA\":\"$(execpath @ninja_1.11.0_win//:ninja_bin)\"}",
"x86_64-pc-windows-gnullvm": "{\"NINJA\":\"$(execpath @ninja_1.11.0_win//:ninja_bin)\"}",
},
crate = "webrtc-audio-processing-sys",
gen_build_script = "on",
patch_args = ["-p1"],
patches = [
"//patches:webrtc-audio-processing-sys_meson_env.patch",
"//patches:webrtc-audio-processing-sys_logging_macos_sdk.patch",
],
)
inject_repo(crate, "meson", "ninja_1.11.0_linux", "ninja_1.12.1_linux_aarch64", "ninja_1.11.0_mac", "ninja_1.11.0_win")
bazel_dep(name = "v8", version = "14.6.202.9")
archive_override(
module_name = "v8",
+5 -11
View File
File diff suppressed because one or more lines are too long
+52 -41
View File
@@ -185,6 +185,18 @@ dependencies = [
"generic-array",
]
[[package]]
name = "aec3"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d2bf40d3b8182532b3e9135b9dda500ae1489fae677bd5ffbd75e38b51761a1"
dependencies = [
"crossbeam-channel",
"log",
"num-complex",
"rustfft",
]
[[package]]
name = "aes"
version = "0.8.4"
@@ -790,15 +802,6 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "autotools"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef941527c41b0fc0dd48511a8154cd5fc7e29200a0ff8b7203c5d777dbc795cf"
dependencies = [
"cc",
]
[[package]]
name = "aws-lc-rs"
version = "1.16.2"
@@ -2865,6 +2868,7 @@ dependencies = [
name = "codex-tui"
version = "0.0.0"
dependencies = [
"aec3",
"anyhow",
"arboard",
"assert_matches",
@@ -2952,7 +2956,6 @@ dependencies = [
"uuid",
"vt100",
"webbrowser",
"webrtc-audio-processing",
"which 8.0.0",
"windows-sys 0.52.0",
"winsplit",
@@ -7789,6 +7792,15 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "primal-check"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08"
dependencies = [
"num-integer",
]
[[package]]
name = "primeorder"
version = "0.13.6"
@@ -8924,6 +8936,20 @@ dependencies = [
"semver",
]
[[package]]
name = "rustfft"
version = "6.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89"
dependencies = [
"num-complex",
"num-integer",
"num-traits",
"primal-check",
"strength_reduce",
"transpose",
]
[[package]]
name = "rusticata-macros"
version = "4.1.0"
@@ -10201,6 +10227,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
[[package]]
name = "strength_reduce"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
[[package]]
name = "string_cache"
version = "0.8.9"
@@ -11164,6 +11196,16 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "transpose"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e"
dependencies = [
"num-integer",
"strength_reduce",
]
[[package]]
name = "tree-sitter"
version = "0.25.10"
@@ -11889,37 +11931,6 @@ dependencies = [
"webrtc-util",
]
[[package]]
name = "webrtc-audio-processing"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31cc7cbf143d9c3de985b113ddc3fddb4a60c7746635a69114f94a5b5af8f42"
dependencies = [
"webrtc-audio-processing-config",
"webrtc-audio-processing-sys",
]
[[package]]
name = "webrtc-audio-processing-config"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb5a1fcf911c54bf3d0e022020f1626ae99add5728c65d345bacfbf3adbf2ef2"
[[package]]
name = "webrtc-audio-processing-sys"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbd9fcfbedf79b0c7bc0f627d4e9cd5efcb648dcfc271c15503194482b39b0e8"
dependencies = [
"anyhow",
"autotools",
"bindgen",
"cc",
"fs_extra",
"pkg-config",
"regex",
]
[[package]]
name = "webrtc-data"
version = "0.17.1"
+1 -1
View File
@@ -197,6 +197,7 @@ ansi-to-tui = "7.0.0"
anyhow = "1"
arboard = { version = "3", features = ["wayland-data-control"] }
arc-swap = "1.9.0"
aec3 = "0.1.7"
assert_cmd = "2"
assert_matches = "1.5.0"
async-channel = "2.3.1"
@@ -352,7 +353,6 @@ vt100 = "0.16.2"
walkdir = "2.5.0"
webbrowser = "1.0"
webrtc = "0.17.1"
webrtc-audio-processing = { version = "~2.0", features = ["bundled"] }
which = "8"
wildmatch = "2.6.1"
zip = "2.4.2"
+1 -1
View File
@@ -112,8 +112,8 @@ codex-windows-sandbox = { workspace = true }
tokio-util = { workspace = true, features = ["time"] }
[target.'cfg(not(target_os = "linux"))'.dependencies]
aec3 = { workspace = true }
cpal = "0.15"
webrtc-audio-processing = { workspace = true }
[target.'cfg(unix)'.dependencies]
libc = { workspace = true }
+5
View File
@@ -155,6 +155,11 @@ impl AppServerSession {
}
}
pub(crate) fn with_remote_cwd_override(mut self, remote_cwd_override: Option<PathBuf>) -> Self {
self.remote_cwd_override = remote_cwd_override;
self
}
pub(crate) fn remote_cwd_override(&self) -> Option<&std::path::Path> {
self.remote_cwd_override.as_deref()
}
+143 -44
View File
@@ -579,15 +579,42 @@ fn latest_session_lookup_params(
source_kinds: (!include_non_interactive)
.then_some(vec![ThreadSourceKind::Cli, ThreadSourceKind::VsCode]),
archived: Some(false),
cwd: if is_remote {
None
} else {
cwd_filter.map(|cwd| cwd.to_string_lossy().to_string())
},
cwd: cwd_filter.map(|cwd| cwd.to_string_lossy().to_string()),
search_term: None,
}
}
fn config_cwd_for_app_server_target(
cwd: Option<&Path>,
app_server_target: &AppServerTarget,
) -> std::io::Result<AbsolutePathBuf> {
if matches!(app_server_target, AppServerTarget::Remote { .. }) {
return AbsolutePathBuf::current_dir();
}
match cwd {
Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?),
None => AbsolutePathBuf::current_dir(),
}
}
fn latest_session_cwd_filter<'a>(
remote_mode: bool,
remote_cwd_override: Option<&'a Path>,
config: &'a Config,
show_all: bool,
) -> Option<&'a Path> {
if show_all {
return None;
}
if remote_mode {
remote_cwd_override
} else {
Some(config.cwd.as_path())
}
}
pub async fn run_main(
mut cli: Cli,
arg0_paths: Arg0DispatchPaths,
@@ -606,6 +633,10 @@ pub async fn run_main(
auth_token: remote_auth_token.clone(),
})
.unwrap_or(AppServerTarget::Embedded);
let remote_cwd_override = cli
.cwd
.clone()
.filter(|_| matches!(app_server_target, AppServerTarget::Remote { .. }));
let (sandbox_mode, approval_policy) = if cli.full_auto {
(
Some(SandboxMode::WorkspaceWrite),
@@ -656,10 +687,7 @@ pub async fn run_main(
};
let cwd = cli.cwd.clone();
let config_cwd = match cwd.as_deref() {
Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?)?,
None => AbsolutePathBuf::current_dir()?,
};
let config_cwd = config_cwd_for_app_server_target(cwd.as_deref(), &app_server_target)?;
#[allow(clippy::print_stderr)]
let config_toml = match load_config_as_toml_with_cli_overrides(
@@ -747,7 +775,11 @@ pub async fn run_main(
model,
approval_policy,
sandbox_mode,
cwd,
cwd: if matches!(app_server_target, AppServerTarget::Remote { .. }) {
None
} else {
cwd
},
model_provider: model_provider_override.clone(),
config_profile: cli.config_profile.clone(),
codex_self_exe: arg0_paths.codex_self_exe.clone(),
@@ -909,6 +941,7 @@ pub async fn run_main(
arg0_paths,
loader_overrides,
app_server_target,
remote_cwd_override,
config,
overrides,
cli_kv_overrides,
@@ -927,6 +960,7 @@ async fn run_ratatui_app(
arg0_paths: Arg0DispatchPaths,
loader_overrides: LoaderOverrides,
app_server_target: AppServerTarget,
remote_cwd_override: Option<PathBuf>,
initial_config: Config,
overrides: ConfigOverrides,
cli_kv_overrides: Vec<(String, toml::Value)>,
@@ -985,18 +1019,21 @@ async fn run_ratatui_app(
let needs_onboarding_app_server =
should_show_trust_screen_flag || initial_config.model_provider.requires_openai_auth;
let mut onboarding_app_server = if needs_onboarding_app_server {
Some(AppServerSession::new(
start_app_server(
&app_server_target,
arg0_paths.clone(),
initial_config.clone(),
cli_kv_overrides.clone(),
loader_overrides.clone(),
cloud_requirements.clone(),
feedback.clone(),
Some(
AppServerSession::new(
start_app_server(
&app_server_target,
arg0_paths.clone(),
initial_config.clone(),
cli_kv_overrides.clone(),
loader_overrides.clone(),
cloud_requirements.clone(),
feedback.clone(),
)
.await?,
)
.await?,
))
.with_remote_cwd_override(remote_cwd_override.clone()),
)
} else {
None
};
@@ -1099,18 +1136,21 @@ async fn run_ratatui_app(
|| cli.resume_picker
|| cli.fork_picker;
let mut session_lookup_app_server = if needs_app_server_session_lookup {
Some(AppServerSession::new(
start_app_server(
&app_server_target,
arg0_paths.clone(),
config.clone(),
cli_kv_overrides.clone(),
loader_overrides.clone(),
cloud_requirements.clone(),
feedback.clone(),
Some(
AppServerSession::new(
start_app_server(
&app_server_target,
arg0_paths.clone(),
config.clone(),
cli_kv_overrides.clone(),
loader_overrides.clone(),
cloud_requirements.clone(),
feedback.clone(),
)
.await?,
)
.await?,
))
.with_remote_cwd_override(remote_cwd_override.clone()),
)
} else {
None
};
@@ -1129,12 +1169,21 @@ async fn run_ratatui_app(
}
}
} else if cli.fork_last {
let filter_cwd = if remote_mode {
latest_session_cwd_filter(
remote_mode,
remote_cwd_override.as_deref(),
&config,
cli.fork_show_all,
)
} else {
None
};
let Some(app_server) = session_lookup_app_server.as_mut() else {
unreachable!("session lookup app server should be initialized for --fork --last");
};
match lookup_latest_session_target_with_app_server(
app_server, &config, /*cwd_filter*/ None,
/*include_non_interactive*/ false,
app_server, &config, filter_cwd, /*include_non_interactive*/ false,
)
.await?
{
@@ -1181,11 +1230,12 @@ async fn run_ratatui_app(
}
}
} else if cli.resume_last {
let filter_cwd = if cli.resume_show_all {
None
} else {
Some(config.cwd.as_path())
};
let filter_cwd = latest_session_cwd_filter(
remote_mode,
remote_cwd_override.as_deref(),
&config,
cli.resume_show_all,
);
let Some(app_server) = session_lookup_app_server.as_mut() else {
unreachable!("session lookup app server should be initialized for --resume --last");
};
@@ -1336,7 +1386,7 @@ async fn run_ratatui_app(
let app_result = App::run(
&mut tui,
AppServerSession::new(app_server),
AppServerSession::new(app_server).with_remote_cwd_override(remote_cwd_override),
config,
cli_kv_overrides.clone(),
overrides.clone(),
@@ -1797,12 +1847,9 @@ mod tests {
-> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let config = build_config(&temp_dir).await?;
let cwd = temp_dir.path().join("project");
let params = latest_session_lookup_params(
/*is_remote*/ true,
&config,
Some(cwd.as_path()),
/*is_remote*/ true, &config, /*cwd_filter*/ None,
/*include_non_interactive*/ false,
);
@@ -1811,6 +1858,58 @@ mod tests {
Ok(())
}
#[tokio::test]
async fn latest_session_lookup_params_keep_explicit_cwd_filter_for_remote_sessions()
-> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let config = build_config(&temp_dir).await?;
let cwd = Path::new("repo/on/server");
let params = latest_session_lookup_params(
/*is_remote*/ true,
&config,
Some(cwd),
/*include_non_interactive*/ false,
);
assert_eq!(params.model_providers, None);
assert_eq!(params.cwd.as_deref(), Some("repo/on/server"));
Ok(())
}
#[test]
fn config_cwd_for_app_server_target_uses_current_dir_for_remote_sessions() -> std::io::Result<()>
{
let remote_only_cwd = if cfg!(windows) {
Path::new(r"C:\definitely\not\local\to\this\test")
} else {
Path::new("/definitely/not/local/to/this/test")
};
let target = AppServerTarget::Remote {
websocket_url: "ws://127.0.0.1:1234/".to_string(),
auth_token: None,
};
let config_cwd = config_cwd_for_app_server_target(Some(remote_only_cwd), &target)?;
assert_eq!(config_cwd, AbsolutePathBuf::current_dir()?);
Ok(())
}
#[test]
fn config_cwd_for_app_server_target_canonicalizes_embedded_cli_cwd() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let target = AppServerTarget::Embedded;
let config_cwd = config_cwd_for_app_server_target(Some(temp_dir.path()), &target)?;
assert_eq!(
config_cwd,
AbsolutePathBuf::from_absolute_path(temp_dir.path().canonicalize()?)?
);
Ok(())
}
#[tokio::test]
async fn read_session_cwd_returns_none_without_sqlite_or_rollout_path() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
+139 -78
View File
@@ -1,47 +1,40 @@
use aec3::voip::VoipAec3;
use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::mpsc;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::Sender;
use tracing::warn;
use webrtc_audio_processing::Config as AudioProcessingConfig;
use webrtc_audio_processing::Processor;
use webrtc_audio_processing::config::EchoCanceller;
use webrtc_audio_processing::config::GainController;
use webrtc_audio_processing::config::HighPassFilter;
use webrtc_audio_processing::config::NoiseSuppression;
use webrtc_audio_processing::config::NoiseSuppressionLevel;
use webrtc_audio_processing::config::Pipeline;
pub(crate) const AUDIO_PROCESSING_SAMPLE_RATE: u32 = 24_000;
pub(crate) const AUDIO_PROCESSING_CHANNELS: u16 = 1;
enum AudioProcessorCommand {
Capture(Vec<i16>),
Render(Vec<i16>),
}
#[derive(Clone)]
pub(crate) struct RealtimeAudioProcessor {
processor: Arc<Processor>,
capture_rx: Arc<Mutex<Option<Receiver<Vec<i16>>>>>,
command_tx: Sender<AudioProcessorCommand>,
}
impl RealtimeAudioProcessor {
pub(crate) fn new() -> Result<Self, String> {
let processor = Processor::new(AUDIO_PROCESSING_SAMPLE_RATE)
.map_err(|err| format!("failed to initialize realtime audio processor: {err}"))?;
processor.set_config(AudioProcessingConfig {
pipeline: Pipeline {
multi_channel_capture: false,
multi_channel_render: false,
..Default::default()
},
echo_canceller: Some(EchoCanceller::Full {
stream_delay_ms: None,
}),
noise_suppression: Some(NoiseSuppression {
level: NoiseSuppressionLevel::High,
..Default::default()
}),
gain_controller: Some(GainController::GainController2(Default::default())),
high_pass_filter: Some(HighPassFilter::default()),
..Default::default()
});
processor.set_output_will_be_muted(true);
build_pipeline()?;
let (command_tx, command_rx) = mpsc::channel();
let (capture_tx, capture_rx) = mpsc::channel();
std::thread::Builder::new()
.name("codex-realtime-aec3".to_string())
.spawn(move || run_processor_thread(command_rx, capture_tx))
.map_err(|err| format!("failed to spawn realtime audio processor: {err}"))?;
Ok(Self {
processor: Arc::new(processor),
capture_rx: Arc::new(Mutex::new(Some(capture_rx))),
command_tx,
})
}
@@ -49,13 +42,21 @@ impl RealtimeAudioProcessor {
&self,
input_sample_rate: u32,
input_channels: u16,
) -> RealtimeCaptureAudioProcessor {
RealtimeCaptureAudioProcessor {
processor: self.processor.clone(),
) -> Result<RealtimeCaptureAudioProcessor, String> {
let capture_rx = self
.capture_rx
.lock()
.ok()
.and_then(|mut capture_rx| capture_rx.take())
.ok_or_else(|| "realtime capture stage was already created".to_string())?;
Ok(RealtimeCaptureAudioProcessor {
capture_rx,
command_tx: self.command_tx.clone(),
input_sample_rate,
input_channels,
pending_samples: VecDeque::new(),
}
processed_samples: VecDeque::new(),
})
}
pub(crate) fn render_stage(
@@ -64,23 +65,19 @@ impl RealtimeAudioProcessor {
output_channels: u16,
) -> RealtimeRenderAudioProcessor {
RealtimeRenderAudioProcessor {
processor: self.processor.clone(),
command_tx: self.command_tx.clone(),
output_sample_rate,
output_channels,
pending_samples: VecDeque::new(),
}
}
pub(crate) fn set_output_will_be_muted(&self, muted: bool) {
self.processor.set_output_will_be_muted(muted);
}
}
pub(crate) struct RealtimeCaptureAudioProcessor {
processor: Arc<Processor>,
capture_rx: Receiver<Vec<i16>>,
command_tx: Sender<AudioProcessorCommand>,
input_sample_rate: u32,
input_channels: u16,
pending_samples: VecDeque<i16>,
processed_samples: VecDeque<i16>,
}
impl RealtimeCaptureAudioProcessor {
@@ -92,40 +89,37 @@ impl RealtimeCaptureAudioProcessor {
AUDIO_PROCESSING_SAMPLE_RATE,
AUDIO_PROCESSING_CHANNELS,
);
self.pending_samples.extend(converted);
let mut processed = Vec::new();
while self.pending_samples.len() >= self.processor.num_samples_per_frame() {
let mut frame = self.pop_pending_frame();
if let Err(err) = self.processor.process_capture_frame([frame.as_mut_slice()]) {
warn!("failed to process realtime capture audio: {err}");
continue;
}
processed.extend(frame.into_iter().map(f32_to_i16));
if !converted.is_empty()
&& let Err(err) = self
.command_tx
.send(AudioProcessorCommand::Capture(converted))
{
warn!("failed to queue realtime capture audio: {err}");
}
processed
}
fn pop_pending_frame(&mut self) -> Vec<f32> {
self.pending_samples
.drain(..self.processor.num_samples_per_frame())
.map(i16_to_f32)
.collect()
loop {
match self.capture_rx.try_recv() {
Ok(processed) => self.processed_samples.extend(processed),
Err(mpsc::TryRecvError::Empty) => break,
Err(mpsc::TryRecvError::Disconnected) => {
warn!("realtime capture audio processor disconnected");
break;
}
}
}
self.processed_samples.drain(..).collect()
}
}
pub(crate) struct RealtimeRenderAudioProcessor {
processor: Arc<Processor>,
command_tx: Sender<AudioProcessorCommand>,
output_sample_rate: u32,
output_channels: u16,
pending_samples: VecDeque<i16>,
}
impl RealtimeRenderAudioProcessor {
pub(crate) fn process_samples(&mut self, samples: &[i16]) {
self.processor
.set_output_will_be_muted(samples.iter().all(|sample| *sample == 0));
let converted = convert_pcm16(
samples,
self.output_sample_rate,
@@ -133,22 +127,91 @@ impl RealtimeRenderAudioProcessor {
AUDIO_PROCESSING_SAMPLE_RATE,
AUDIO_PROCESSING_CHANNELS,
);
self.pending_samples.extend(converted);
if !converted.is_empty()
&& let Err(err) = self
.command_tx
.send(AudioProcessorCommand::Render(converted))
{
warn!("failed to queue realtime render audio: {err}");
}
}
}
while self.pending_samples.len() >= self.processor.num_samples_per_frame() {
let mut frame = self.pop_pending_frame();
if let Err(err) = self.processor.process_render_frame([frame.as_mut_slice()]) {
warn!("failed to process realtime render audio: {err}");
fn build_pipeline() -> Result<VoipAec3, String> {
VoipAec3::builder(
AUDIO_PROCESSING_SAMPLE_RATE as usize,
usize::from(AUDIO_PROCESSING_CHANNELS),
usize::from(AUDIO_PROCESSING_CHANNELS),
)
.enable_high_pass(false)
.enable_noise_suppression(false)
.build()
.map_err(|err| format!("failed to initialize realtime audio processor: {err}"))
}
fn run_processor_thread(command_rx: Receiver<AudioProcessorCommand>, capture_tx: Sender<Vec<i16>>) {
let mut pipeline = match build_pipeline() {
Ok(pipeline) => pipeline,
Err(err) => {
warn!("{err}");
return;
}
};
let capture_frame_len =
pipeline.capture_frame_samples() * usize::from(AUDIO_PROCESSING_CHANNELS);
let render_frame_len = pipeline.render_frame_samples() * usize::from(AUDIO_PROCESSING_CHANNELS);
let mut pending_capture = VecDeque::new();
let mut pending_render = VecDeque::new();
while let Ok(command) = command_rx.recv() {
match command {
AudioProcessorCommand::Capture(samples) => {
pending_capture.extend(samples);
while pending_capture.len() >= capture_frame_len {
let capture_frame =
drain_pending_frame(&mut pending_capture, capture_frame_len);
let capture_frame = capture_frame
.iter()
.copied()
.map(i16_to_f32)
.collect::<Vec<_>>();
let mut output = vec![0.0; capture_frame.len()];
if let Err(err) = pipeline.process_capture_frame(
&capture_frame,
/*level_change*/ false,
&mut output,
) {
warn!("failed to process realtime capture audio: {err}");
continue;
}
let processed = output.into_iter().map(f32_to_i16).collect::<Vec<_>>();
if let Err(err) = capture_tx.send(processed) {
warn!("failed to deliver realtime capture audio: {err}");
return;
}
}
}
AudioProcessorCommand::Render(samples) => {
pending_render.extend(samples);
while pending_render.len() >= render_frame_len {
let render_frame = drain_pending_frame(&mut pending_render, render_frame_len);
let render_frame = render_frame
.iter()
.copied()
.map(i16_to_f32)
.collect::<Vec<_>>();
if let Err(err) = pipeline.handle_render_frame(&render_frame) {
warn!("failed to process realtime render audio: {err}");
}
}
}
}
}
}
fn pop_pending_frame(&mut self) -> Vec<f32> {
self.pending_samples
.drain(..self.processor.num_samples_per_frame())
.map(i16_to_f32)
.collect()
}
fn drain_pending_frame(pending_samples: &mut VecDeque<i16>, frame_len: usize) -> Vec<i16> {
pending_samples.drain(..frame_len).collect()
}
pub(crate) fn convert_pcm16(
@@ -210,12 +273,10 @@ pub(crate) fn convert_pcm16(
out
}
#[inline]
fn i16_to_f32(sample: i16) -> f32 {
(sample as f32) / (i16::MAX as f32)
}
#[inline]
fn f32_to_i16(sample: f32) -> i16 {
(sample.clamp(-1.0, 1.0) * i16::MAX as f32) as i16
}
+1 -6
View File
@@ -45,7 +45,7 @@ impl VoiceCapture {
&device,
&config,
tx,
audio_processor.capture_stage(sample_rate, channels),
audio_processor.capture_stage(sample_rate, channels)?,
last_peak.clone(),
)?;
stream
@@ -318,7 +318,6 @@ fn convert_u16_to_i16_and_peak(input: &[u16], out: &mut Vec<i16>) -> u16 {
pub(crate) struct RealtimeAudioPlayer {
_stream: cpal::Stream,
queue: Arc<Mutex<VecDeque<i16>>>,
audio_processor: RealtimeAudioProcessor,
output_sample_rate: u32,
output_channels: u16,
}
@@ -345,7 +344,6 @@ impl RealtimeAudioPlayer {
Ok(Self {
_stream: stream,
queue,
audio_processor,
output_sample_rate,
output_channels,
})
@@ -381,8 +379,6 @@ impl RealtimeAudioPlayer {
.map_err(|_| "failed to lock output audio queue".to_string())?;
// TODO(aibrahim): Cap or trim this queue if we observe producer bursts outrunning playback.
guard.extend(converted);
drop(guard);
self.audio_processor.set_output_will_be_muted(false);
Ok(())
}
@@ -390,7 +386,6 @@ impl RealtimeAudioPlayer {
if let Ok(mut guard) = self.queue.lock() {
guard.clear();
}
self.audio_processor.set_output_will_be_muted(true);
}
}
-2
View File
@@ -19,8 +19,6 @@ exports_files([
"v8_bazel_rules.patch",
"v8_module_deps.patch",
"v8_source_portability.patch",
"webrtc-audio-processing-sys_logging_macos_sdk.patch",
"webrtc-audio-processing-sys_meson_env.patch",
"windows-link.patch",
"zstd-sys_windows_msvc_include_dirs.patch",
])
@@ -1,12 +0,0 @@
diff --git a/webrtc-audio-processing/webrtc/rtc_base/logging.cc b/webrtc-audio-processing/webrtc/rtc_base/logging.cc
--- a/webrtc-audio-processing/webrtc/rtc_base/logging.cc
+++ b/webrtc-audio-processing/webrtc/rtc_base/logging.cc
@@ -24,6 +24,6 @@
#undef ERROR // wingdi.h
#endif
#if defined(WEBRTC_MAC) && !defined(WEBRTC_IOS)
-#include <CoreServices/CoreServices.h>
+#include <CoreFoundation/CoreFoundation.h>
#elif defined(WEBRTC_ANDROID)
#include <android/log.h>
@@ -1,406 +0,0 @@
diff --git a/build.rs b/build.rs
index 8578e9220a..bd5c9ba382 100644
--- a/build.rs
+++ b/build.rs
@@ -1,10 +1,14 @@
use anyhow::{bail, Context, Result};
use bindgen::callbacks::{AttributeInfo, DeriveInfo, ParseCallbacks};
+#[cfg(not(windows))]
+use std::os::unix::fs::symlink;
+#[cfg(windows)]
+use std::os::windows::fs::symlink_dir as symlink;
use std::{
env,
- fs::File,
+ fs::{self, File},
io::{BufWriter, Write},
- path::PathBuf,
+ path::{Path, PathBuf},
process::Command,
};
@@ -203,8 +207,136 @@ mod webrtc {
let webrtc_build_dir = build_dir.join(BUNDLED_SOURCE_PATH);
eprintln!("Building webrtc-audio-processing in {}", webrtc_build_dir.display());
- let mut meson = Command::new("meson");
+ let meson_launcher = env::var_os("MESON").expect("MESON is set by Bazel");
+ let cargo_manifest_dir =
+ env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Bazel");
+ let runfiles_dir = Path::new(&cargo_manifest_dir)
+ .parent()
+ .expect("CARGO_MANIFEST_DIR points inside a Bazel runfiles tree");
+ let staged_meson = build_dir.join("meson");
+ fs::copy(&meson_launcher, &staged_meson).context("Failed to stage Meson launcher")?;
+ let meson_archive = PathBuf::from(&meson_launcher).with_extension("zip");
+ if meson_archive.exists() {
+ fs::copy(&meson_archive, build_dir.join("meson.zip"))
+ .context("Failed to stage Meson support archive")?;
+ }
+ let mut meson_runfiles = staged_meson.as_os_str().to_owned();
+ meson_runfiles.push(".runfiles");
+ let _ = fs::remove_file(&meson_runfiles);
+ symlink(runfiles_dir, &meson_runfiles).context("Failed to link Meson runfiles")?;
+
+ let mut meson = Command::new(staged_meson);
+ let mut meson_cross_file = None;
+ if let (Ok(host), Ok(target)) = (env::var("HOST"), env::var("TARGET")) {
+ if host != target {
+ let mut target_parts = target.split('-');
+ let cpu_family = target_parts
+ .next()
+ .expect("TARGET triple has an architecture");
+ let system = target_parts
+ .next_back()
+ .expect("TARGET triple has an operating system");
+ let endian = match cpu_family {
+ "aarch64" | "x86_64" => "little",
+ _ => bail!("Unsupported TARGET architecture for Meson cross file: {cpu_family}"),
+ };
+ let cross_file = build_dir.join("meson-cross-file.ini");
+ fs::write(
+ &cross_file,
+ format!(
+ concat!(
+ "[host_machine]\n",
+ "system = '{system}'\n",
+ "cpu_family = '{cpu_family}'\n",
+ "cpu = '{cpu_family}'\n",
+ "endian = '{endian}'\n",
+ "\n",
+ "[properties]\n",
+ "needs_exe_wrapper = true\n",
+ ),
+ system = system,
+ cpu_family = cpu_family,
+ endian = endian,
+ ),
+ )
+ .context("Failed to write Meson cross file")?;
+ meson_cross_file = Some(cross_file);
+ }
+ }
+
+ let mut ninja_launcher = env::var_os("NINJA");
+ if ninja_launcher.is_none() {
+ for candidate in ["ninja", "ninja-build"] {
+ let can_execute = Command::new(candidate)
+ .arg("--version")
+ .status()
+ .map(|status| status.success())
+ .unwrap_or(false);
+ if can_execute {
+ ninja_launcher = Some(candidate.into());
+ break;
+ }
+ }
+ }
+ if let Some(ninja_real) = env::var_os("NINJA_REAL") {
+ let mut temp_dirs = Vec::new();
+ if let Some(dir) = env::var_os("CODEX_NINJA_TMPDIR") {
+ temp_dirs.push(PathBuf::from(dir));
+ }
+ temp_dirs.extend([
+ PathBuf::from("/private/var/tmp"),
+ PathBuf::from("/var/tmp"),
+ env::temp_dir(),
+ PathBuf::from("/tmp"),
+ ]);
+ if let Some(home) = env::var_os("HOME") {
+ temp_dirs.push(PathBuf::from(home));
+ }
+
+ let mut staged_ninja = None;
+ for (attempt, temp_dir) in temp_dirs.into_iter().enumerate() {
+ if !temp_dir.is_dir() {
+ continue;
+ }
+
+ let stage_dir =
+ temp_dir.join(format!("codex-ninja-{}-{attempt}", std::process::id()));
+ let _ = fs::remove_dir_all(&stage_dir);
+ if fs::create_dir_all(&stage_dir).is_err() {
+ continue;
+ }
+
+ let candidate_ninja = stage_dir.join("ninja");
+ let stage_result =
+ fs::copy(&ninja_real, &candidate_ninja).and_then(|_| ensure_executable(&candidate_ninja));
+ if stage_result.is_ok() {
+ staged_ninja = Some(candidate_ninja.into_os_string());
+ break;
+ }
+ }
+ ninja_launcher = staged_ninja.or_else(|| Some(ninja_real));
+ }
+ let ninja_launcher =
+ ninja_launcher.ok_or_else(|| anyhow::anyhow!("Failed to locate ninja launcher"))?;
+
+ if let Some(ldflags) = env::var("LDFLAGS").ok() {
+ let execroot = determine_execroot(runfiles_dir)?;
+ let ldflags = ldflags
+ .split_whitespace()
+ .map(|token| {
+ if let Some(path) = token.strip_prefix("-Lbazel-out") {
+ format!("-L{}", execroot.join(format!("bazel-out{path}")).display())
+ } else if token.starts_with("bazel-out") {
+ execroot.join(token).display().to_string()
+ } else {
+ token.to_owned()
+ }
+ })
+ .collect::<Vec<_>>()
+ .join(" ");
+ meson.env("LDFLAGS", ldflags);
+ }
meson.args(["setup", "--prefix", install_dir.to_str().unwrap()]);
+ meson.env("NINJA", &ninja_launcher);
+ if let Some(cross_file) = meson_cross_file {
+ meson.arg("--cross-file");
+ meson.arg(cross_file);
+ }
meson.arg("--reconfigure");
if cfg!(target_os = "macos") {
@@ -221,14 +353,14 @@ mod webrtc {
.context("Failed to execute meson. Do you have it installed?")?;
assert!(status.success(), "Command failed: {:?}", &meson);
- let mut ninja = Command::new("ninja");
+ let mut ninja = Command::new(ninja_launcher.clone());
let status = ninja
.current_dir(&webrtc_build_dir)
.status()
.context("Failed to execute ninja. Do you have it installed?")?;
assert!(status.success(), "Command failed: {:?}", &ninja);
- let mut install = Command::new("ninja");
+ let mut install = Command::new(ninja_launcher);
let status = install
.current_dir(&webrtc_build_dir)
.arg("install")
@@ -267,15 +399,32 @@ mod webrtc {
/// Extract defined (non-external) symbols from a static library using nm.
fn get_defined_symbols(archive_path: &std::path::Path) -> Result<Vec<String>> {
- let output = Command::new("nm")
+ let nm = determine_nm_path();
+ let output = match Command::new(&nm)
.arg("--defined-only")
.arg("--format=posix")
.arg(archive_path)
.output()
- .context("Failed to execute nm")?;
+ {
+ Ok(output) => output,
+ Err(error)
+ if env::var("CARGO_CFG_TARGET_OS")
+ .map(|target_os| target_os == "macos")
+ .or_else(|_| env::var("TARGET").map(|target| target.contains("apple-darwin")))
+ .unwrap_or(false)
+ && error.kind() == std::io::ErrorKind::NotFound =>
+ {
+ println!(
+ "cargo:warning=Skipping symbol prefixing because {:?} is unavailable",
+ nm
+ );
+ return Ok(Vec::new());
+ }
+ Err(error) => return Err(error).context(format!("Failed to execute {:?}", nm)),
+ };
if !output.status.success() {
- anyhow::bail!("nm failed: {}", String::from_utf8_lossy(&output.stderr));
+ anyhow::bail!("{:?} failed: {}", nm, String::from_utf8_lossy(&output.stderr));
}
let stdout = String::from_utf8_lossy(&output.stdout);
@@ -400,6 +549,7 @@ fn main() -> Result<()> {
}
let binding_file = out_dir().join("bindings.rs");
+ maybe_set_libclang_path();
let mut builder = bindgen::Builder::default()
.header("src/wrapper.hpp")
.clang_args(&["-x", "c++", "-std=c++17", "-fparse-all-comments"])
@@ -461,3 +611,180 @@ fn determine_objcopy_path() -> Result<PathBuf> {
Ok(objcopy)
}
+
+fn determine_execroot(path: &Path) -> Result<PathBuf> {
+ for ancestor in path.ancestors() {
+ if ancestor.file_name().and_then(|name| name.to_str()) == Some("bazel-out") {
+ return ancestor
+ .parent()
+ .map(Path::to_path_buf)
+ .context("bazel-out directory is missing its execroot parent");
+ }
+ }
+
+ env::current_dir().context("Failed to determine execroot")
+}
+
+#[cfg(not(windows))]
+fn ensure_executable(path: &Path) -> std::io::Result<()> {
+ use std::os::unix::fs::PermissionsExt;
+
+ let mut permissions = fs::metadata(path)?.permissions();
+ permissions.set_mode(0o755);
+ fs::set_permissions(path, permissions)
+}
+
+#[cfg(windows)]
+fn ensure_executable(_path: &Path) -> std::io::Result<()> {
+ Ok(())
+}
+
+fn determine_nm_path() -> PathBuf {
+ if let Some(nm) = env::var_os("NM") {
+ return nm.into();
+ }
+
+ if env::var("CARGO_CFG_TARGET_OS")
+ .map(|target_os| target_os == "macos")
+ .or_else(|_| env::var("TARGET").map(|target| target.contains("apple-darwin")))
+ .unwrap_or(false)
+ {
+ let xcrun_output = Command::new("/usr/bin/xcrun")
+ .args(["--find", "llvm-nm"])
+ .output();
+ if let Ok(output) = xcrun_output {
+ if output.status.success() {
+ let path = String::from_utf8_lossy(&output.stdout).trim().to_owned();
+ if !path.is_empty() {
+ return PathBuf::from(path);
+ }
+ }
+ }
+ }
+
+ for tool_env in ["AR", "CC", "CXX"] {
+ if let Some(tool_path) = env::var_os(tool_env) {
+ let tool_path = PathBuf::from(tool_path);
+ if let Some(parent) = tool_path.parent() {
+ for candidate_name in ["llvm-nm", "nm"] {
+ let candidate = parent.join(candidate_name);
+ if candidate.exists() {
+ return candidate;
+ }
+ }
+ }
+
+ if let Some(file_name) = tool_path.file_name().and_then(|name| name.to_str()) {
+ let candidate_name = file_name.replace("llvm-ar", "llvm-nm");
+ if candidate_name != file_name {
+ let candidate = tool_path.with_file_name(candidate_name);
+ if candidate.exists() {
+ return candidate;
+ }
+ }
+
+ if file_name != "llvm-nm" {
+ let candidate = tool_path.with_file_name("llvm-nm");
+ if candidate.exists() {
+ return candidate;
+ }
+ }
+ }
+ }
+ }
+
+ for candidate in ["llvm-nm", "nm"] {
+ if Command::new(candidate)
+ .arg("--version")
+ .status()
+ .map(|status| status.success())
+ .unwrap_or(false)
+ {
+ return PathBuf::from(candidate);
+ }
+ }
+
+ PathBuf::from("nm")
+}
+
+fn maybe_set_libclang_path() {
+ if env::var_os("LIBCLANG_PATH").is_some() {
+ return;
+ }
+
+ for tool_env in ["CC", "CXX"] {
+ let Some(tool_value) = env::var_os(tool_env) else {
+ continue;
+ };
+ let tool_path = PathBuf::from(tool_value);
+ let tool_path = if tool_path.is_absolute() {
+ tool_path
+ } else if let Ok(current_dir) = env::current_dir() {
+ let current_dir_candidate = current_dir.join(&tool_path);
+ if current_dir_candidate.exists() {
+ current_dir_candidate
+ } else if let Some(manifest_dir) = env::var_os("CARGO_MANIFEST_DIR").map(PathBuf::from) {
+ let mut resolved = tool_path.clone();
+ for ancestor in manifest_dir.ancestors() {
+ let ancestor_candidate = ancestor.join(&tool_path);
+ if ancestor_candidate.exists() {
+ resolved = ancestor_candidate;
+ break;
+ }
+ }
+ resolved
+ } else {
+ tool_path
+ }
+ } else {
+ tool_path
+ };
+
+ let Some(tool_dir) = tool_path.parent() else {
+ continue;
+ };
+ let Some(tool_root) = tool_dir.parent() else {
+ continue;
+ };
+
+ let mut directories = vec![tool_root.join("lib"), tool_root.join("lib64")];
+ if let Ok(read_dir) = fs::read_dir(tool_root) {
+ directories.extend(
+ read_dir
+ .filter_map(std::result::Result::ok)
+ .map(|entry| entry.path())
+ .filter(|path| path.is_dir()),
+ );
+ }
+
+ while let Some(directory) = directories.pop() {
+ let read_dir = match fs::read_dir(&directory) {
+ Ok(read_dir) => read_dir,
+ Err(_) => continue,
+ };
+
+ for entry in read_dir.filter_map(std::result::Result::ok) {
+ let path = entry.path();
+ if path.is_dir() {
+ directories.push(path);
+ continue;
+ }
+
+ let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
+ continue;
+ };
+ let is_libclang = matches!(file_name, "libclang.so" | "libclang.dll" | "libclang.dylib")
+ || file_name.starts_with("libclang.so.")
+ || file_name.starts_with("libclang-");
+ if !is_libclang {
+ continue;
+ }
+
+ if let Some(parent) = path.parent() {
+ env::set_var("LIBCLANG_PATH", parent);
+ return;
+ }
+ }
+ }
+ }
+}
-4
View File
@@ -1,4 +0,0 @@
exports_files(
["ninja_wrapper.sh"],
visibility = ["//visibility:public"],
)
-30
View File
@@ -1,30 +0,0 @@
#!/bin/sh
set -eu
real_ninja="${NINJA_REAL:?NINJA_REAL must point to the downloaded ninja binary}"
try_exec() {
candidate_dir="$1"
[ -n "${candidate_dir}" ] || return 1
[ -d "${candidate_dir}" ] || return 1
staged_dir="$(mktemp -d "${candidate_dir%/}/codex-ninja.XXXXXX" 2>/dev/null)" || return 1
staged_ninja="${staged_dir}/ninja"
if cp "${real_ninja}" "${staged_ninja}" 2>/dev/null \
&& chmod +x "${staged_ninja}" 2>/dev/null \
&& "${staged_ninja}" --version >/dev/null 2>&1; then
exec "${staged_ninja}" "$@"
fi
rm -rf "${staged_dir}"
return 1
}
for candidate_dir in "${CODEX_NINJA_TMPDIR:-}" /private/var/tmp /var/tmp "${TMPDIR:-}" /tmp "${HOME:-}"; do
try_exec "${candidate_dir}"
done
echo "codex ninja wrapper: unable to stage an executable Ninja binary" >&2
exit 1