//! # `chanora_audio` //! //! Audio subsystem promoted from `poc/audio-capture-playback-spike` //! and wired against `chanora_protocol`'s voice channels. //! //! ## What's wired in this Beta //! //! * 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 platform output callback at 48 kHz stereo //! * Push-to-talk: capture stream is permanently open; encoding is //! gated by an atomic `ptt_active` flag //! //! ## Voice processing in this Beta //! //! * iOS/macOS use Apple's VoiceProcessingIO path, which owns platform //! AEC / AGC / noise suppression for the shipping route. //! * Rust owns VoiceActivity transmit gating and exposes a software //! processor surface for debug/future raw routes. //! * Hot-plug device-change handling is still platform-specific follow-up work. //! * Sample-rate adaptation if the device cannot do 48 kHz / mono in //! the format we request (returns `AudioError::StreamConfig`) //! * Multi-channel speaker layouts beyond stereo #![warn(missing_docs)] #[cfg(any(target_os = "android", test))] #[cfg_attr(not(target_os = "android"), allow(dead_code))] mod android_render_ring; #[cfg(any(target_os = "android", test))] #[cfg_attr(not(target_os = "android"), allow(dead_code))] mod audio_event_queue; pub mod audio_processing; #[cfg_attr(not(target_os = "android"), allow(dead_code))] mod capture_accumulator; #[cfg_attr(not(target_os = "android"), allow(dead_code))] mod capture_resampler; pub mod debug_wav; mod engine; pub mod frame; pub mod mobile_voice_backend; pub mod mode_stack; pub(crate) mod opus_voice; pub mod processor; pub mod ptt; pub mod ptt_backends; pub mod release_tail; #[cfg_attr( not(any(target_os = "android", target_os = "ios", test)), allow(dead_code) )] pub(crate) mod render_reference; pub mod route_policy; pub mod transmit_mode; pub mod transmit_selector; pub mod vad; pub mod voice_activity; pub(crate) mod voice_render; #[cfg(target_os = "linux")] mod sdl_output; #[cfg(any(target_os = "ios", target_os = "macos"))] mod ios_voice_unit; #[cfg(target_os = "ios")] pub mod ios_raw_unit; #[cfg(target_os = "android")] pub mod android_voice_unit; pub use audio_processing::{ AudioBackend, AudioProcessingConfig, AudioProcessingStats, AudioRoute, EffectOwner, IosVoiceProcessingMode, SharedAudioProcessingStats, VadBackend, }; pub use engine::list_audio_devices; pub use engine::{AudioDeviceInfo, AudioDeviceList, AudioEngine, AudioEngineConfig}; // SDD-120 ยง3 bench seam โ€” `#[doc(hidden)]` re-export so the criterion // 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 = "macos", target_os = "android")))] #[doc(hidden)] pub use engine::bench_seam; pub use ptt::{AudioTransmitGate, MissedKeyUpWatchdog, PttBackendDescriptor, PttCapabilityLevel}; pub use ptt_backends::{ select as select_ptt_backend, DesktopPttBackend, FocusedPttBackend, PttBackendError, PttBinding, PttInputClass, }; pub use release_tail::{ReleaseTailTimer, DEFAULT_TAIL_MS, MAX_TAIL_MS}; pub use transmit_mode::TransmitMode; pub use transmit_selector::{PermissionGate, TransmitModeSelector}; use thiserror::Error; /// Errors raised by the audio subsystem. #[derive(Debug, Error)] pub enum AudioError { /// Platform did not expose a usable default input device. #[error("no default input device")] NoInputDevice, /// Platform did not expose a usable default output device. #[error("no default output device")] NoOutputDevice, /// The audio backend rejected a stream configuration. #[error("stream config rejected: {0}")] StreamConfig(String), /// Opus codec init/encode/decode failure. #[error("opus: {0}")] Opus(String), /// A backend-specific failure surfaced without a typed mapping. #[error("audio backend: {0}")] Backend(String), /// Android platform not ready โ€” ndk_context not initialised before /// audio engine start. This occurs when `initChanoraContext` has not /// been called before `voice_join` triggers the audio engine. #[error("android platform not ready: ndk_context not initialised")] PlatformNotReady, /// Audio processing config failed validation. #[error("invalid audio processing config: {0}")] InvalidAudioProcessingConfig(String), /// Requested audio processing config is schema-visible but not implemented. #[error("unsupported audio processing config: {0}")] UnsupportedAudioProcessingConfig(String), } /// Audio-effect toggles. Defaults match DEC-007 (AEC), /// DEC-008 (AGC), DEC-009 (NS), DEC-010 (HPF) โ€” all enabled. /// /// On iOS/macOS these map to VoiceProcessingIO-owned effects in the /// default route. Software processor backends may also consult them /// on raw/debug routes. #[derive(Debug, Clone, Copy)] pub struct AudioEffects { /// Acoustic echo cancellation (DEC-007). pub aec: bool, /// Automatic gain control (DEC-008). pub agc: bool, /// Noise suppression (DEC-009). pub noise_suppression: bool, /// High-pass filter (DEC-010). pub high_pass: bool, } impl Default for AudioEffects { fn default() -> Self { Self { aec: true, agc: true, noise_suppression: true, high_pass: true, } } } #[cfg(test)] mod tests { use super::*; #[test] fn defaults_match_decisions() { let e = AudioEffects::default(); assert!(e.aec && e.agc && e.noise_suppression && e.high_pass); } }