diff --git a/crates/chanora_audio/Cargo.toml b/crates/chanora_audio/Cargo.toml index b396ab5..97c6ac6 100644 --- a/crates/chanora_audio/Cargo.toml +++ b/crates/chanora_audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chanora_audio" -description = "Chanora audio subsystem — cpal-based capture/playback, audiopus encode, tsclientlib AudioHandler for decode + jitter buffer + mix. DEC-011, DEC-011.1." +description = "Chanora audio subsystem — platform-native capture/playback, audiopus encode, tsclientlib AudioHandler for decode + jitter buffer + mix. DEC-011, DEC-011.1." version.workspace = true edition.workspace = true rust-version.workspace = true @@ -14,39 +14,32 @@ chanora_protocol = { path = "../chanora_protocol" } thiserror.workspace = true tracing.workspace = true -# Cross-platform audio I/O (DEC-011.1). -cpal = "0.17.3" # Opus encoder. tsclientlib already pulls this; we depend explicitly so # this crate can compile against it without going through tsclientlib. audiopus = "0.3.0-rc.0" - # AudioHandler lives in the tsclientlib crate behind the `audio` # feature. We import the crate just for the AudioHandler type; the # Connection type stays inside chanora_protocol. tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["audio"] } tokio = { version = "1", features = ["sync", "rt", "macros", "time"] } +[target.'cfg(all(not(target_os = "android"), not(target_os = "ios"), not(target_os = "macos")))'.dependencies] +# 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" + [target.'cfg(not(target_os = "android"))'.dependencies] # Desktop/iOS: native TLS maps to the platform TLS backend (Security.framework # on Apple, SChannel on Windows, system OpenSSL on Linux/BSD). reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "native-tls"] } -[target.'cfg(target_os = "ios")'.dependencies] -# Direct CoreAudio AudioUnit access on iOS (DEC-011.x follow-up). -# cpal's iOS backend is unsuitable for VoIP: it opens -# kAudioUnitSubType_RemoteIO with a mono-only output element and no -# control over buffer size / sample rate, AND its AudioUnit stays -# bound to the route present at construction time so user-driven -# `overrideOutputAudioPort` flips do not actually move audio to the -# new transducer. Every production iOS VoIP client (Linphone, Mumble -# iOS, Signal, Jitsi, WebRTC reference) instead drives -# `kAudioUnitSubType_VoiceProcessingIO` (a.k.a. VPIO) directly. VPIO -# is Apple's recommended voice unit; it ships hardware AEC + AGC + NS -# and honours route changes natively because it IS the canonical -# voice unit on iOS. `coreaudio-rs` (RustAudio org, same maintainers -# as `cpal`, 8.6M downloads) gives us a safe wrapper around the -# AudioUnit C API. We use it on iOS only; cpal stays on macOS where -# its CoreAudio backend works well against HAL units. +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] +# Direct CoreAudio AudioUnit access on Apple platforms (DEC-011 follow-up). +# cpal's Apple path does not expose the voice-processing controls Chanora +# needs for VoIP. Use `kAudioUnitSubType_VoiceProcessingIO` directly via +# `coreaudio-rs` so capture/playback share Apple's native AEC + AGC + NS +# voice unit on both iOS and macOS. # # Default features keep `audio_toolbox` + `core_audio`, both required # for AudioUnit construction + property access. @@ -137,8 +130,9 @@ futures-util = { version = "0.3", default-features = false, features = ["std"] } # options. The portal recommends fresh tokens to scope its own # object paths per call. rand = "0.8" -# SDL2 audio for Linux. Replaces the cpal capture / playback paths -# on Linux only; cpal stays in use on Windows/macOS. Rationale: the +# 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 # cpal Linux backend opens raw ALSA `default`, which on most Arch # / Fedora / Debian installs routes through `dmix` + `plug` with # nearest-neighbour resampling and very small period sizes — the diff --git a/crates/chanora_audio/src/android_voice_unit.rs b/crates/chanora_audio/src/android_voice_unit.rs index 3fe6025..599800f 100644 --- a/crates/chanora_audio/src/android_voice_unit.rs +++ b/crates/chanora_audio/src/android_voice_unit.rs @@ -42,10 +42,7 @@ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; use audiopus::coder::Encoder as OpusEncoder; -use audiopus::{ - Application as OpusApp, Bitrate as OpusBitrate, Channels as OpusChannels, - SampleRate as OpusSampleRate, -}; +use audiopus::{Application as OpusApp, Channels as OpusChannels, SampleRate as OpusSampleRate}; use tracing::{debug, info, warn}; use crate::mobile_voice_backend::{ @@ -340,9 +337,7 @@ pub struct AndroidVoiceUnit { output_perf: AchievedPerformanceMode, output_share: AchievedSharingMode, input_sample_rate: i32, - output_sample_rate: i32, input_frames_per_burst: i32, - output_frames_per_burst: i32, session_id: Option, @@ -397,7 +392,7 @@ impl AndroidVoiceUnit { let scratch = Arc::new(Mutex::new(Vec::with_capacity(8192))); // --- Open input stream (SDD-112) --------------------------- - let mut input_builder = AudioStreamBuilder::default() + let input_builder = AudioStreamBuilder::default() .set_direction::() .set_sample_rate(cfg.sample_rate as i32) .set_channel_count::() @@ -583,9 +578,7 @@ impl AndroidVoiceUnit { output_perf, output_share, input_sample_rate, - output_sample_rate, input_frames_per_burst, - output_frames_per_burst, session_id, hw_effects, event_tx, diff --git a/crates/chanora_audio/src/engine.rs b/crates/chanora_audio/src/engine.rs index aff5831..364aee5 100644 --- a/crates/chanora_audio/src/engine.rs +++ b/crates/chanora_audio/src/engine.rs @@ -8,9 +8,17 @@ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; -#[cfg(all(not(target_os = "ios"), not(target_os = "android")))] +#[cfg(all( + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "android") +))] use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -#[cfg(all(not(target_os = "ios"), not(target_os = "android")))] +#[cfg(all( + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "android") +))] use cpal::{SampleFormat, SizedSample}; use tokio::sync::mpsc; use tracing::{debug, info}; @@ -18,15 +26,27 @@ use tracing::{debug, info}; // playback paths (`build_input_stream`, `build_output_stream`, // `try_open_capture` log lines). Cfg-gate the imports too so iOS // builds don't carry an unused-imports warning. -#[cfg(all(not(target_os = "ios"), not(target_os = "android")))] +#[cfg(all( + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "android") +))] use tracing::{error, warn}; #[cfg(target_os = "android")] use tracing::warn; -#[cfg(all(not(target_os = "ios"), not(target_os = "android")))] +#[cfg(all( + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "android") +))] use audiopus::coder::Encoder as OpusEncoder; -#[cfg(all(not(target_os = "ios"), not(target_os = "android")))] +#[cfg(all( + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "android") +))] use audiopus::{ Application as OpusApp, Bitrate as OpusBitrate, Channels as OpusChannels, SampleRate as OpusSampleRate, @@ -41,7 +61,11 @@ use tsclientlib::audio::AudioHandler; // iOS too, and `OutPacket` flows out of the capture pipeline once // commit 3 lands. Cfg-gate the cpal-only ones to keep iOS warnings // clean. -#[cfg(all(not(target_os = "ios"), not(target_os = "android")))] +#[cfg(all( + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "android") +))] use chanora_protocol::{AudioData, CodecType, OutAudio}; use chanora_protocol::{InboundVoice, OutPacket}; @@ -87,13 +111,11 @@ pub struct AudioEngineConfig { /// ignored — the DSP chain stays a no-op and we use the /// default ALSA/PipeWire source. /// - /// Beta status: the *config flag* is plumbed through every - /// layer; the *Android-side preset switch* is documented but - /// not yet wired through cpal, which currently uses the - /// AAudio default input. RISK-AUDIO-MOBILE-001 tracks this gap. - /// Setting `true` is a forward-compatible no-op for Beta and - /// will become active once cpal exposes input-preset hooks (or - /// when Chanora ships an Oboe-based fork). + /// Android status: this is enforced by the Oboe-only backend, + /// which opens input with `VoiceCommunication` and output with + /// voice-communication usage / speech content. Setting `false` + /// is rejected on Android because the P0 path intentionally has + /// no generic mobile-audio fallback. pub mobile_voice_preset: bool, } @@ -139,25 +161,28 @@ pub struct AudioEngine { // VoiceProcessingIO AudioUnit hosts mic + speaker) — cpal is // unused on iOS for the reasons documented in // `ios_voice_unit.rs`. - #[cfg(all(not(target_os = "ios"), not(target_os = "android")))] + #[cfg(all( + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "android") + ))] _input_stream: Mutex>, #[cfg(target_os = "linux")] _output_stream: Mutex>, #[cfg(all( not(target_os = "linux"), not(target_os = "ios"), + not(target_os = "macos"), not(target_os = "android") ))] _output_stream: Mutex>, - #[cfg(target_os = "ios")] + #[cfg(any(target_os = "ios", target_os = "macos"))] _ios_voice_unit: Mutex>, - /// SDD-111..SDD-115: Android voice backend held parallel to the - /// cpal streams. Owns the SDD-113 hardware-effect handles and - /// the foreground-service lifecycle; tearing it down on engine - /// drop unbinds effects and stops the service in SDD-115 - /// reverse order. The cpal capture/playback pair continues to - /// carry voice frames for the transitional period — replacing - /// the data path with the Oboe streams is a follow-up. + /// SDD-111..SDD-115: Android Oboe voice backend. Owns the input + /// and output streams, SDD-113 hardware-effect handles, and the + /// foreground-service lifecycle; tearing it down on engine drop + /// unbinds effects and stops the service in SDD-115 reverse + /// order. #[cfg(target_os = "android")] _android_voice_unit: Mutex>, /// SDD-108 §1/§2: refcount-composable audio-mode controller. @@ -234,12 +259,11 @@ impl AudioEngine { voice_in_rx: mpsc::Receiver, transmit_gate: crate::ptt::AudioTransmitGate, ) -> Result { - // iOS routes to a separate backend (VoiceProcessingIO via - // coreaudio-rs) because cpal's iOS RemoteIO path produces - // mono-only output bound to a stale physical transducer - // (see `ios_voice_unit.rs` for the long version). Every - // other platform stays on the cpal / SDL flow below. - #[cfg(target_os = "ios")] + // Apple platforms route to a separate backend (VoiceProcessingIO + // via coreaudio-rs) because cpal does not expose the native + // voice-processing AudioUnit controls Chanora needs for VoIP. + // Windows and Linux stay on the cpal / SDL flow below. + #[cfg(any(target_os = "ios", target_os = "macos"))] { return Self::start_with_gate_ios(cfg, voice_out_tx, voice_in_rx, transmit_gate); } @@ -247,18 +271,18 @@ impl AudioEngine { { return Self::start_with_gate_android(cfg, voice_out_tx, voice_in_rx, transmit_gate); } - #[cfg(not(any(target_os = "ios", target_os = "android")))] + #[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] { Self::start_with_gate_cpal(cfg, voice_out_tx, voice_in_rx, transmit_gate) } } - /// Non-iOS implementation: cpal capture + (cpal | SDL2) output. - /// Kept as a separate function so the iOS path can short-circuit - /// at the top of `start_with_gate` without dragging a 200-line + /// Non-Apple/non-Android implementation: cpal capture + (cpal | SDL2) output. + /// Kept as a separate function so the Apple and Android paths can + /// short-circuit at the top of `start_with_gate` without dragging a 200-line /// cfg-gated block. Body is the pre-iOS-port code, unchanged /// except for the new function name + signature. - #[cfg(not(any(target_os = "ios", target_os = "android")))] + #[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] fn start_with_gate_cpal( cfg: AudioEngineConfig, voice_out_tx: mpsc::Sender, @@ -394,13 +418,8 @@ impl AudioEngine { // frames (~46 ms @ 44.1 kHz) gives the Opus decode // callback enough headroom while still being well // under voice-chat latency tolerance. - // * macOS (CoreAudio via cpal): the default period - // is fine and the OS picks a HAL-friendly size. - // * iOS (CoreAudio via cpal): RemoteIO units reject - // arbitrary buffer-size requests and surface them - // as `build_output_stream: The requested stream - // configuration is not supported by the device`. - // Must use BufferSize::Default. + // * Apple platforms do not reach this cpal path; they + // use direct VoiceProcessingIO AudioUnits. #[cfg(target_os = "windows")] let buffer_size = cpal::BufferSize::Fixed(2048); #[cfg(not(target_os = "windows"))] @@ -667,14 +686,14 @@ impl AudioEngine { }) } - /// iOS implementation: a single VoiceProcessingIO AudioUnit + /// Apple implementation: a single VoiceProcessingIO AudioUnit /// drives mic capture + speaker playback (see /// `ios_voice_unit.rs` for why cpal is unsuitable on iOS). /// This mirrors `start_with_gate_cpal` in scaffolding — /// atomics, audio handler, inbound forwarder task — but /// replaces the two cpal stream constructions with a single /// `IosVoiceUnit::start` call. - #[cfg(target_os = "ios")] + #[cfg(any(target_os = "ios", target_os = "macos"))] fn start_with_gate_ios( cfg: AudioEngineConfig, voice_out_tx: mpsc::Sender, @@ -683,10 +702,10 @@ impl AudioEngine { ) -> Result { info!( target: "chanora_audio", - "starting audio engine: iOS VoiceProcessingIO backend" + "starting audio engine: Apple VoiceProcessingIO backend" ); - // iOS AVAudioSession configuration is performed Swift-side + // Apple audio-session configuration is performed Swift-side // in `apps/chanora_flutter/ios/Runner/AppDelegate.swift` // BEFORE Flutter starts its audio pipeline. The category + // mode pair set there (`.playAndRecord` + `.default`) is @@ -796,12 +815,16 @@ impl AudioEngine { // audio. iOS collapses input + output into one // `IosVoiceUnit` (see `ios_voice_unit.rs`); every other // platform has separate cpal input + cpal/SDL output. - #[cfg(all(not(target_os = "ios"), not(target_os = "android")))] + #[cfg(all( + not(target_os = "ios"), + not(target_os = "macos"), + not(target_os = "android") + ))] { let _ = self._input_stream.lock().unwrap().take(); let _ = self._output_stream.lock().unwrap().take(); } - #[cfg(target_os = "ios")] + #[cfg(any(target_os = "ios", target_os = "macos"))] { let _ = self._ios_voice_unit.lock().unwrap().take(); } @@ -1036,7 +1059,7 @@ impl Drop for AudioEngine { // ---------- Capture pipeline ---------- -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] fn try_open_capture( in_dev: &cpal::Device, voice_out_tx: mpsc::Sender, @@ -1144,7 +1167,7 @@ fn try_open_capture( Ok(stream) } -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] struct CaptureState { encoder: OpusEncoder, in_sample_rate: u32, @@ -1182,7 +1205,7 @@ struct CaptureState { frame_scratch: Vec, } -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] impl CaptureState { fn new( encoder: OpusEncoder, @@ -1366,30 +1389,30 @@ impl CaptureState { } /// Per-sample format conversion to f32 in the range [-1.0, 1.0]. -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] trait ToF32 { fn to_f32_sample(self) -> f32; } -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] impl ToF32 for f32 { fn to_f32_sample(self) -> f32 { self } } -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] impl ToF32 for i16 { fn to_f32_sample(self) -> f32 { f32::from(self) / f32::from(i16::MAX) } } -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] impl ToF32 for u16 { fn to_f32_sample(self) -> f32 { (f32::from(self) - f32::from(i16::MAX) - 1.0) / f32::from(i16::MAX) } } -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] fn build_input_stream( device: &cpal::Device, config: &cpal::StreamConfig, @@ -1417,7 +1440,7 @@ where // ---------- Playback pipeline ---------- #[cfg(not(target_os = "linux"))] -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] fn build_output_stream( device: &cpal::Device, config: &cpal::StreamConfig, @@ -1606,7 +1629,7 @@ where /// Resampler state carried across output cpal callbacks. See /// `build_output_stream` for the rationale. #[cfg(not(target_os = "linux"))] -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] struct PlaybackResampleState { pos: f64, last_l: f32, @@ -1614,26 +1637,26 @@ struct PlaybackResampleState { } #[cfg(not(target_os = "linux"))] -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] trait FromF32 { fn from_f32_sample(v: f32) -> Self; } #[cfg(not(target_os = "linux"))] -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] impl FromF32 for f32 { fn from_f32_sample(v: f32) -> Self { v } } #[cfg(not(target_os = "linux"))] -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] impl FromF32 for i16 { fn from_f32_sample(v: f32) -> Self { (v.clamp(-1.0, 1.0) * f32::from(i16::MAX)) as i16 } } #[cfg(not(target_os = "linux"))] -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] impl FromF32 for u16 { fn from_f32_sample(v: f32) -> Self { let s = (v.clamp(-1.0, 1.0) * f32::from(i16::MAX)) as i32; @@ -1822,10 +1845,10 @@ pub fn android_set_audio_mode(mode: i32) -> Result<(), AudioModeError> { // can drive the same realtime capture code path the production cpal // callback uses, without re-implementing CaptureState in the bench file. // Marked `#[doc(hidden)]` so the public API surface is unaffected; this -// is not a supported external API. Only compiled on non-iOS targets -// because `CaptureState` itself is gated on `cfg(not(target_os = "ios"))`. +// is not a supported external API. Only compiled on cpal-capture targets +// because Apple and Android use native voice backends instead. // --------------------------------------------------------------------------- -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] #[doc(hidden)] pub mod bench_seam { use super::{ diff --git a/crates/chanora_audio/src/ios_voice_unit.rs b/crates/chanora_audio/src/ios_voice_unit.rs index 1f04645..9aa8194 100644 --- a/crates/chanora_audio/src/ios_voice_unit.rs +++ b/crates/chanora_audio/src/ios_voice_unit.rs @@ -1,9 +1,11 @@ -//! iOS output + capture stream via the **VoiceProcessingIO** +//! Apple output + capture stream via the **VoiceProcessingIO** //! AudioUnit (`kAudioUnitSubType_VoiceProcessingIO`, a.k.a. VPIO). //! -//! ## Why not cpal on iOS +//! ## Why not cpal on Apple voice paths //! -//! cpal's iOS backend opens `kAudioUnitSubType_RemoteIO` with no +//! cpal's Apple backend does not expose the voice-processing unit controls +//! Chanora needs for a VoIP client. On iOS, cpal opens +//! `kAudioUnitSubType_RemoteIO` with no //! control over the stream format, buffer size, or channel count; //! on iPhone 16 Pro running iOS 18 it reports the output element as //! **mono 48 kHz** even when the session category is `.playAndRecord` @@ -17,20 +19,14 @@ //! in logs but produces no audible difference — the audio is still //! coming out the earpiece. //! -//! Every production iOS VoIP client (Mumble iOS, Linphone / -//! mediastreamer2, Signal-iOS, Jitsi, the WebRTC reference impl) -//! avoids RemoteIO and drives VPIO directly instead. VPIO is -//! Apple's recommended voice unit: it ships hardware AEC + AGC + -//! NS, it accepts arbitrary stream-format requests on bus 0 -//! (output) and bus 1 (input), and it re-binds its underlying HAL -//! transducer correctly when the AVAudioSession route changes, -//! because it IS the canonical voice unit on iOS — Apple's own -//! FaceTime audio path runs through it. +//! Production VoIP clients on Apple platforms drive VPIO directly instead +//! of treating CoreAudio as a generic music-playback device. VPIO is Apple's +//! native voice unit: it ships hardware AEC + AGC + NS and accepts explicit +//! stream-format requests on bus 0 (output) and bus 1 (input). //! -//! This file replaces the cpal capture + playback streams on iOS -//! only. macOS continues to use cpal's CoreAudio HAL backend (which -//! works correctly for desktop audio). Linux uses SDL2 (see -//! `sdl_output.rs`). Windows uses cpal's WASAPI backend. +//! This file replaces the cpal capture + playback streams on iOS and macOS. +//! Linux uses SDL2 for output (see `sdl_output.rs`) and cpal for capture; +//! Windows uses cpal's WASAPI backend. //! //! ## What VPIO gives us //! @@ -663,6 +659,7 @@ impl IosVoiceUnit { /// Route rebinding on iOS is most reliable when we bounce the /// VoiceProcessingIO unit through an uninitialize/reinitialize /// cycle, then start again. + #[cfg(target_os = "ios")] pub fn restart(&mut self) -> Result<(), AudioError> { self.unit .stop() @@ -681,6 +678,7 @@ impl IosVoiceUnit { } /// Pause the audio unit during an interruption. + #[cfg(target_os = "ios")] pub fn pause(&mut self) -> Result<(), AudioError> { self.unit .stop() @@ -688,6 +686,7 @@ impl IosVoiceUnit { } /// Resume the audio unit after an interruption. + #[cfg(target_os = "ios")] pub fn resume(&mut self) -> Result<(), AudioError> { self.unit .start() diff --git a/crates/chanora_audio/src/lib.rs b/crates/chanora_audio/src/lib.rs index b73bda0..a4da376 100644 --- a/crates/chanora_audio/src/lib.rs +++ b/crates/chanora_audio/src/lib.rs @@ -5,12 +5,13 @@ //! //! ## What's wired in this Beta //! -//! * Default-input capture via `cpal` (DEC-011.1) +//! * Default-input capture via platform audio backends (Oboe on Android, +//! VoiceProcessingIO on Apple platforms, cpal/SDL elsewhere) //! * Frame-aligned 20 ms / 48 kHz mono Opus encoding via `audiopus` //! * Forward encoded frames to the protocol crate as `OutPacket`s //! * Inbound voice packets fed to `tsclientlib::audio::AudioHandler` //! which owns Opus decode + per-client jitter buffer + mix -//! * Mixed f32 PCM pulled by the cpal output callback at 48 kHz stereo +//! * Mixed f32 PCM pulled by the platform output callback at 48 kHz stereo //! * Push-to-talk: capture stream is permanently open; encoding is //! gated by an atomic `ptt_active` flag //! @@ -19,8 +20,6 @@ //! * AEC / AGC / NS / HPF DSP chain (DEC-007/008/009/010 — Beta+ //! work; the toggles in `AudioEffects` are honoured by *naming* //! but the filters are no-ops) -//! * Mobile audio paths (DEC-011.1 desktop + Android proven; this -//! integration is desktop-only for v0.2.0-beta.1) //! * Hot-plug device-change handling //! * Sample-rate adaptation if the device cannot do 48 kHz / mono in //! the format we request (returns `AudioError::StreamConfig`) @@ -40,7 +39,7 @@ pub mod transmit_selector; #[cfg(target_os = "linux")] mod sdl_output; -#[cfg(target_os = "ios")] +#[cfg(any(target_os = "ios", target_os = "macos"))] mod ios_voice_unit; #[cfg(target_os = "android")] @@ -52,7 +51,7 @@ pub use engine::{AudioEngine, AudioEngineConfig}; // bench harness under `crates/chanora_audio/benches/` can construct a // CaptureState and drive `ingest` without re-implementing the engine. // Not part of the supported public API. -#[cfg(not(any(target_os = "ios", target_os = "android")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))] #[doc(hidden)] pub use engine::bench_seam; pub use ptt::{AudioTransmitGate, MissedKeyUpWatchdog, PttBackendDescriptor, PttCapabilityLevel}; diff --git a/crates/chanora_audio/src/sdl_output.rs b/crates/chanora_audio/src/sdl_output.rs index 959765a..00652bc 100644 --- a/crates/chanora_audio/src/sdl_output.rs +++ b/crates/chanora_audio/src/sdl_output.rs @@ -26,9 +26,9 @@ //! //! This file replaces the cpal output path on Linux only. The //! capture path stays on cpal until we have a reason to swap it -//! (the user reports outbound is currently fine). Windows / -//! macOS continue to use cpal because cpal's WASAPI and -//! CoreAudio backends do not have this problem. +//! (the user reports outbound is currently fine). Windows continues +//! to use cpal's WASAPI backend; Apple platforms use direct +//! VoiceProcessingIO AudioUnits for the voice path. //! //! ## Threading & lifecycle //!