From d59da05f9355ca858e5bfbc8b0988d45789a10d7 Mon Sep 17 00:00:00 2001 From: Edison Jwa Date: Sun, 7 Jun 2026 18:56:18 +0900 Subject: [PATCH] refactor(audio): share AudioHandler between iOS and macOS, bump deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit crates/chanora_audio/src/engine.rs: drop the macOS-specific event-queue producer/consumer path; macOS now uses the iOS-style direct AudioHandler::fill_buffer in the VPIO render callback. The shared AudioHandler is an Arc>; the realtime callback uses try_lock so it never blocks on the tokio decode task (see ios_voice_unit.rs render callback). crates/chanora_audio/src/mobile_voice_backend.rs: update VoiceAudioParams cfg gates — handler is now the iOS/macOS/desktop shape (Arc>>), event_producer is Android-only. crates/chanora_audio/src/lib.rs: widen the audio_event_queue module visibility to test so the macOS-specific path can be exercised by the unit test suite. Cargo.toml: bump cpal 0.17.3 -> 0.18.0, jni 0.21 -> 0.22.4, windows 0.54 -> 0.62, criterion 0.5 -> 0.8. Cargo.lock follows. --- Cargo.lock | 144 +++++++++--------- crates/chanora_audio/Cargo.toml | 12 +- crates/chanora_audio/src/engine.rs | 46 ++---- crates/chanora_audio/src/lib.rs | 3 +- .../chanora_audio/src/mobile_voice_backend.rs | 15 +- 5 files changed, 101 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eff8065..7748b20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,6 +70,15 @@ dependencies = [ "backtrace", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "alsa" version = "0.11.0" @@ -428,12 +437,12 @@ dependencies = [ "dhat", "dispatch2", "futures-util", - "jni 0.21.1", + "jni 0.22.4", "ndarray", "ndk-context", "oboe", "ort", - "rand 0.8.6", + "rand 0.10.1", "rustfft", "sdl2", "serde_json", @@ -444,7 +453,7 @@ dependencies = [ "tracing-subscriber", "tsclientlib", "webrtc-vad", - "windows 0.54.0", + "windows", "zbus", ] @@ -717,14 +726,15 @@ dependencies = [ [[package]] name = "cpal" -version = "0.17.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7" +checksum = "d9dd2b2151ebb4d5866c804d89fe28244bfb6b74481b9b4d406e4ec4d7f88ce5" dependencies = [ "alsa", + "block2", "coreaudio-rs", "dasp_sample", - "jni 0.21.1", + "jni 0.22.4", "js-sys", "libc", "mach2", @@ -739,10 +749,9 @@ dependencies = [ "objc2-core-audio-types", "objc2-core-foundation", "objc2-foundation", - "wasm-bindgen", - "wasm-bindgen-futures", "web-sys", - "windows 0.62.2", + "windows", + "windows-core", ] [[package]] @@ -765,25 +774,24 @@ dependencies = [ [[package]] name = "criterion" -version = "0.5.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ + "alloca", "anes", "cast", "ciborium", "clap", "criterion-plot", - "is-terminal", - "itertools 0.10.5", + "itertools 0.13.0", "num-traits", - "once_cell", "oorandom", + "page_size", "plotters", "rayon", "regex", "serde", - "serde_derive", "serde_json", "tinytemplate", "walkdir", @@ -791,12 +799,12 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.5.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", - "itertools 0.10.5", + "itertools 0.13.0", ] [[package]] @@ -1985,7 +1993,7 @@ dependencies = [ "socket2", "widestring", "windows-registry", - "windows-result 0.4.1", + "windows-result", "windows-sys 0.61.2", ] @@ -1998,22 +2006,11 @@ dependencies = [ "serde", ] -[[package]] -name = "is-terminal" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "itertools" -version = "0.10.5" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -2238,12 +2235,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "mach2" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" -dependencies = [ - "libc", -] +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" [[package]] name = "matchers" @@ -2540,6 +2534,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" dependencies = [ + "bitflags 2.12.1", "objc2", "objc2-foundation", ] @@ -2752,6 +2747,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parking" version = "2.2.1" @@ -3498,9 +3503,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sdl2" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b498da7d14d1ad6c839729bd4ad6fc11d90a57583605f3b4df2cd709a9cd380" +checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef" dependencies = [ "bitflags 1.3.2", "lazy_static", @@ -3510,9 +3515,9 @@ dependencies = [ [[package]] name = "sdl2-sys" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951deab27af08ed9c6068b7b0d05a93c91f0a8eb16b6b816a5e73452a43521d3" +checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0" dependencies = [ "cfg-if", "libc", @@ -4768,6 +4773,22 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -4778,14 +4799,10 @@ dependencies = [ ] [[package]] -name = "windows" -version = "0.54.0" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-targets 0.52.6", -] +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" @@ -4794,7 +4811,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", - "windows-core 0.62.2", + "windows-core", "windows-future", "windows-numerics", ] @@ -4805,17 +4822,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-core 0.62.2", -] - -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", + "windows-core", ] [[package]] @@ -4827,7 +4834,7 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link", - "windows-result 0.4.1", + "windows-result", "windows-strings", ] @@ -4837,7 +4844,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "windows-core 0.62.2", + "windows-core", "windows-link", "windows-threading", ] @@ -4876,7 +4883,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-core 0.62.2", + "windows-core", "windows-link", ] @@ -4887,19 +4894,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ "windows-link", - "windows-result 0.4.1", + "windows-result", "windows-strings", ] -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.4.1" diff --git a/crates/chanora_audio/Cargo.toml b/crates/chanora_audio/Cargo.toml index 174666d..c2759c2 100644 --- a/crates/chanora_audio/Cargo.toml +++ b/crates/chanora_audio/Cargo.toml @@ -35,7 +35,7 @@ crossbeam = { version = "0.8", default-features = false, features = ["alloc", "c # Desktop audio I/O for Windows capture/playback and Linux capture. # Linux playback uses SDL2; Apple platforms use direct VoiceProcessingIO # AudioUnits via `coreaudio-rs` for the voice path. -cpal = "0.17.3" +cpal = "0.18.0" [target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] # Direct CoreAudio AudioUnit access on Apple platforms (DEC-011 follow-up). @@ -58,7 +58,7 @@ ort = { version = "2.0.0-rc.12", default-features = false, features = ["load-dyn # JNI bindings to flip Android's AudioManager into MODE_IN_COMMUNICATION # when the voice-comm preset is requested. ndk_context is initialised # by the bridge crate's android_init shim. -jni = { version = "0.21", default-features = false } +jni = { version = "0.22.4", default-features = false } ndk-context = "0.1" # Oboe-rs (Google Oboe wrapper) for low-latency voice capture + playback. # Primary backend for SDD-111..SDD-115. The pre-compiled static library @@ -84,7 +84,7 @@ bytemuck = { version = "1", features = ["derive"] } # + WM_INPUT translation backed by a hidden message-only window, and # SetWindowsHookExW(WH_KEYBOARD_LL / WH_MOUSE_LL) fallback. Both # require a per-backend OS thread that owns a message pump. -windows = { version = "0.54", features = [ +windows = { version = "0.62", features = [ "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_System_LibraryLoader", @@ -104,7 +104,7 @@ tracing-subscriber = { version = "0.3", features = ["registry"] } # SDD-120 §3 — criterion bench harness (realtime_capture / opus_codec / # resampler). `harness = false` per bench entry below disables the # default libtest harness so criterion can install its own. -criterion = "0.5" +criterion = "0.8" # SDD-120 §3 item 1 — dhat is used as the global allocator inside # `benches/realtime_capture.rs` to count post-warmup heap allocations # on the realtime capture path. Dev-dep only — does NOT affect @@ -147,7 +147,7 @@ futures-util = { version = "0.3", default-features = false, features = ["std"] } # Random token bytes for the portal handle_token / session_handle_token # options. The portal recommends fresh tokens to scope its own # object paths per call. -rand = "0.8" +rand = "0.10" # SDL2 audio for Linux. Replaces the cpal playback path on Linux only; # cpal stays in use for Linux capture and Windows capture/playback. # Apple platforms use direct VoiceProcessingIO AudioUnits. Rationale: the @@ -166,4 +166,4 @@ rand = "0.8" # libSDL2.so. Arch ships `sdl2-compat`; Debian/Ubuntu ship # `libsdl2-2.0-0`; Fedora ships `SDL2`. The chanora-flutter Linux # build documentation lists this as a runtime dependency. -sdl2 = { version = "0.37", default-features = false } +sdl2 = { version = "0.38", default-features = false } diff --git a/crates/chanora_audio/src/engine.rs b/crates/chanora_audio/src/engine.rs index dc4e5a4..bd9adff 100644 --- a/crates/chanora_audio/src/engine.rs +++ b/crates/chanora_audio/src/engine.rs @@ -52,7 +52,7 @@ use tsclientlib::audio::AudioHandler; use chanora_protocol::{InboundVoice, OutPacket}; -#[cfg(any(target_os = "android", target_os = "macos"))] +#[cfg(target_os = "android")] use crate::audio_event_queue::{AudioCommand, AudioEventQueue, AudioPacket}; use crate::AudioError; @@ -307,9 +307,9 @@ pub struct AudioEngine { output_muted: Arc, audio_processing_config: Arc>, audio_processing_stats: Arc, - #[cfg(not(any(target_os = "android", target_os = "macos")))] + #[cfg(not(target_os = "android"))] audio_handler: Arc>>, - #[cfg(any(target_os = "android", target_os = "macos"))] + #[cfg(target_os = "android")] audio_event_producer: crate::audio_event_queue::AudioEventProducer, #[cfg(target_os = "android")] voice_out_tx: mpsc::Sender, @@ -1246,25 +1246,16 @@ impl AudioEngine { let audio_processing_config = Arc::new(Mutex::new(crate::AudioProcessingConfig::default())); let audio_processing_stats = Arc::new(crate::SharedAudioProcessingStats::default()); - #[cfg(target_os = "ios")] + #[cfg(any(target_os = "ios", target_os = "macos"))] let audio_handler: Arc>> = Arc::new(Mutex::new(AudioHandler::new())); - #[cfg(target_os = "macos")] - let event_queue = AudioEventQueue::new(); - #[cfg(target_os = "macos")] - let event_producer = AudioEventQueue::producer(&event_queue); let voice_out_tx_for_backend = voice_out_tx.clone(); // Construct the live iOS voice backend. Platform VPIO stays // the default shipping path; Sonora/RemoteIO remains opt-in. let ios_voice_backend = open_ios_voice_backend(crate::mobile_voice_backend::VoiceAudioParams { - #[cfg(target_os = "ios")] handler: audio_handler.clone(), - #[cfg(target_os = "macos")] - handler: AudioHandler::new(), - #[cfg(target_os = "macos")] - event_producer: event_producer.clone(), output_gain: output_gain.clone(), output_muted: output_muted.clone(), voice_out_tx: voice_out_tx_for_backend, @@ -1289,10 +1280,7 @@ impl AudioEngine { // waiting. let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel(); let frames_received_for_task = frames_received.clone(); - #[cfg(target_os = "ios")] let handler_for_task = audio_handler.clone(); - #[cfg(target_os = "macos")] - let event_producer_for_task = event_producer.clone(); tokio::spawn(async move { loop { tokio::select! { @@ -1304,21 +1292,16 @@ impl AudioEngine { match item { Some(v) => { let id = SessionAudioId(v.from_client); - #[cfg(target_os = "ios")] - { let mut h = handler_for_task.lock().unwrap(); - if let Err(e) = h.handle_packet(id, v.packet) { - debug!(target: "chanora_audio", error = %e, "decode failed"); - } else { - frames_received_for_task.fetch_add(1, Ordering::Relaxed); - } - } - #[cfg(target_os = "macos")] - { - let packet = AudioPacket { client_id: id, data: v.packet }; - if event_producer_for_task.push_packet(packet).is_ok() { + let res = h.handle_packet(id, v.packet); + drop(h); + match res { + Ok(_) => { frames_received_for_task.fetch_add(1, Ordering::Relaxed); } + Err(e) => { + debug!(target: "chanora_audio", error = %e, "decode failed"); + } } } None => break, @@ -1336,10 +1319,7 @@ impl AudioEngine { output_muted, audio_processing_config, audio_processing_stats, - #[cfg(target_os = "ios")] audio_handler, - #[cfg(target_os = "macos")] - audio_event_producer: event_producer, _ios_voice_backend: Mutex::new(Some(ios_voice_backend)), shutdown_tx: Some(shutdown_tx), capture_active, @@ -1693,7 +1673,7 @@ impl AudioEngine { /// `0.0..4.0`. pub fn set_client_volume(&self, client_id: u64, volume: f32) { let clamped = volume.clamp(0.0, 4.0); - #[cfg(any(target_os = "android", target_os = "macos"))] + #[cfg(target_os = "android")] { let mut cmd = AudioCommand::SetVolume(SessionAudioId(client_id), clamped); for _ in 0..64 { @@ -1712,7 +1692,7 @@ impl AudioEngine { "set_client_volume: control queue full after 64 retries — volume not applied" ); } - #[cfg(not(any(target_os = "android", target_os = "macos")))] + #[cfg(not(target_os = "android"))] { match self.audio_handler.lock() { Ok(mut h) => { diff --git a/crates/chanora_audio/src/lib.rs b/crates/chanora_audio/src/lib.rs index 87d24fd..12d36be 100644 --- a/crates/chanora_audio/src/lib.rs +++ b/crates/chanora_audio/src/lib.rs @@ -28,7 +28,8 @@ #![warn(missing_docs)] -#[cfg(any(target_os = "android", target_os = "macos"))] +#[cfg(any(target_os = "android", test))] +#[cfg_attr(not(target_os = "android"), allow(dead_code))] mod audio_event_queue; pub mod audio_processing; pub mod debug_wav; diff --git a/crates/chanora_audio/src/mobile_voice_backend.rs b/crates/chanora_audio/src/mobile_voice_backend.rs index 582088f..3be07e8 100644 --- a/crates/chanora_audio/src/mobile_voice_backend.rs +++ b/crates/chanora_audio/src/mobile_voice_backend.rs @@ -78,15 +78,18 @@ pub(crate) struct VoiceAudioParams { pub frames_sent: Arc, /// Pre-encode amplitude scale (1.0 = unity). pub mic_gain: f32, - /// AudioHandler owned by the Android/macOS output callback. - #[cfg(any(target_os = "android", target_os = "macos"))] + /// AudioHandler owned by the Android output callback. + #[cfg(target_os = "android")] pub handler: AudioHandler, - /// Producer used by Android/macOS engine tasks to feed the output callback. - #[cfg(any(target_os = "android", target_os = "macos"))] + /// Producer used by Android engine tasks to feed the output callback. + #[cfg(target_os = "android")] pub event_producer: crate::audio_event_queue::AudioEventProducer, /// AudioHandler that inbound decode+mix feeds into; the output - /// callback pulls mixed stereo f32 from it. - #[cfg(not(any(target_os = "android", target_os = "macos")))] + /// callback pulls mixed stereo f32 from it. iOS, macOS, and desktop + /// share this `Arc>` shape; the realtime callback uses + /// `try_lock` so it never blocks on the tokio decode task (see + /// `ios_voice_unit.rs` render callback). + #[cfg(not(target_os = "android"))] pub handler: Arc>>, /// Master output gain (f32 bits stored in AtomicU32 for lock-free /// cross-thread read from the realtime audio callback).