diff --git a/crates/chanora_audio/src/engine.rs b/crates/chanora_audio/src/engine.rs index 0957333..2e60e65 100644 --- a/crates/chanora_audio/src/engine.rs +++ b/crates/chanora_audio/src/engine.rs @@ -14,7 +14,10 @@ use tokio::sync::mpsc; use tracing::{debug, error, info, warn}; use audiopus::coder::Encoder as OpusEncoder; -use audiopus::{Application as OpusApp, Channels as OpusChannels, SampleRate as OpusSampleRate}; +use audiopus::{ + Application as OpusApp, Bitrate as OpusBitrate, Channels as OpusChannels, + SampleRate as OpusSampleRate, +}; use tsclientlib::audio::AudioHandler; @@ -624,13 +627,63 @@ fn try_open_capture( in_stream_cfg.buffer_size = cpal::BufferSize::Default; } - let opus_enc = OpusEncoder::new( + let mut opus_enc = OpusEncoder::new( OpusSampleRate::Hz48000, OpusChannels::Mono, OpusApp::Voip, ) .map_err(|e| AudioError::Opus(format!("encoder new: {e}")))?; + // Opus VOIP tuning. Defaults give us 'auto' bitrate (can drop + // to ~6 kbps during silence \u2014 which sounds garbled when + // talking resumes) and inband FEC disabled. On lossy mobile + // networks (cellular / iPhone WiFi roaming), packet loss + // without FEC produces audible clicks + cut-out frames. + // + // Settings derived from the Opus IETF VoIP recommendations + // (RFC 6716 \u00a7 7.1) and Discord's voice client tuning: + // + // * Bitrate 32 kbps : sweet spot for mono voice. Lower + // than 24 kbps starts to sound watery; higher than + // 64 kbps wastes bandwidth without perceptual gain on a + // human voice. Discord uses 64 kbps; mumble defaults to + // 40 kbps; we pick 32 kbps as a conservative VoIP value + // that survives 100 kbps uplinks comfortably. + // * Complexity 10 : max quality. The CPU cost on a modern + // iPhone (A14+) or any desktop is negligible (~0.5 % of + // a single core for 48 kHz mono). + // * Inband FEC on : opus inserts a low-bitrate redundancy + // copy of the previous frame inside the current packet + // so a single dropped packet can be reconstructed from + // the next one. Essential on lossy mobile. + // * Packet loss perc 5 % : tells the encoder to expect 5 % + // loss and pre-emptively budget bits for FEC. Higher + // values trade audio quality for resilience. + // + // Errors here are non-fatal: log + continue. The encoder + // works with defaults if any setter fails on an exotic + // libopus build. + if let Err(e) = opus_enc.set_bitrate(OpusBitrate::BitsPerSecond(32_000)) { + warn!(target: "chanora_audio", error = %e, "opus: set_bitrate(32000) failed"); + } + if let Err(e) = opus_enc.set_complexity(10) { + warn!(target: "chanora_audio", error = %e, "opus: set_complexity(10) failed"); + } + if let Err(e) = opus_enc.set_inband_fec(true) { + warn!(target: "chanora_audio", error = %e, "opus: set_inband_fec(true) failed"); + } + if let Err(e) = opus_enc.set_packet_loss_perc(5) { + warn!(target: "chanora_audio", error = %e, "opus: set_packet_loss_perc(5) failed"); + } + info!( + target: "chanora_audio", + bitrate_bps = 32_000, + complexity = 10, + inband_fec = true, + packet_loss_perc = 5, + "opus encoder tuned for VoIP" + ); + let capture_state = Arc::new(Mutex::new(CaptureState::new( opus_enc, in_sample_rate,