Add W12 Opus/network quality profiles
CI / check (push) Failing after 25s
cargo-deny / cargo-deny (push) Has been cancelled
windows-build / windows-build (push) Has been cancelled

Add a small, named codec-policy picker (Low latency / Balanced / Bad
network) instead of exposing raw Opus knobs. The profile->params mapping
is a pure function (`codec::opus_impl::opus_params`) for unit testing;
profiles tune bitrate, in-band FEC, expected packet-loss, and DTX.

- config: `AudioProfile` enum (serde + Display + ALL + u8 round-trip),
  persisted `audio_profile` field (default Balanced).
- codec: `OpusParams` + pure `opus_params()` + `OpusEncoder::apply_params`
  / `apply_profile`.
- core: new `SetAudioProfile` command (Reliable, no coalesce); a shared
  `AtomicU8` lets the capture thread re-tune the live encoder on a
  mid-call switch and read it at each new call's encoder creation.
- app: Settings "Connection quality" picker in the Audio tab, startup
  config-sync send, and a one-line hint per profile.

No wire-format change (GOSSIP/audio planes untouched). 499 lib tests
green (config + codec mapping/apply tests added), clippy + fmt clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 16:13:26 -04:00
co-authored by Claude Opus 4.8
parent 551767f9f5
commit d92d0f6f6b
5 changed files with 271 additions and 4 deletions
+109 -1
View File
@@ -1,5 +1,49 @@
use crate::codec::{AudioDecoder, AudioEncoder, CodecError};
use opus::{Application, Channels, Decoder, Encoder};
use crate::config::AudioProfile;
use opus::{Application, Bitrate, Channels, Decoder, Encoder};
/// Concrete libopus encoder settings derived from an [`AudioProfile`]. Plain
/// data, so the profile→params mapping ([`opus_params`]) stays a pure,
/// unit-testable function (W12).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OpusParams {
/// Target bitrate in bits/sec.
pub bitrate: i32,
/// Enable in-band forward error correction (loss redundancy in the bitstream).
pub inband_fec: bool,
/// Expected packet-loss percentage (0..=100); tunes how much FEC libopus adds.
pub packet_loss_perc: i32,
/// Discontinuous transmission: stop sending during silence to save bandwidth.
pub dtx: bool,
}
/// Map a named profile to concrete Opus parameters. Pure — the W12 testable seam.
///
/// `BadNetwork` deliberately runs a *lower* bitrate than `Balanced`: in-band FEC
/// redundancy is carried inside the same bitstream, so trimming the base bitrate
/// leaves headroom for the redundancy on a congested link.
pub fn opus_params(profile: AudioProfile) -> OpusParams {
match profile {
AudioProfile::LowLatency => OpusParams {
bitrate: 24_000,
inband_fec: false,
packet_loss_perc: 0,
dtx: false,
},
AudioProfile::Balanced => OpusParams {
bitrate: 32_000,
inband_fec: true,
packet_loss_perc: 10,
dtx: false,
},
AudioProfile::BadNetwork => OpusParams {
bitrate: 20_000,
inband_fec: true,
packet_loss_perc: 25,
dtx: true,
},
}
}
pub struct OpusEncoder {
encoder: Encoder,
@@ -17,6 +61,29 @@ impl OpusEncoder {
.map_err(|e| CodecError::Init(format!("Failed to create Opus encoder: {}", e)))?;
Ok(Self { encoder })
}
/// Apply concrete codec parameters to the live encoder. Safe to call between
/// frames, so the user can switch profile mid-call.
pub fn apply_params(&mut self, params: &OpusParams) -> Result<(), CodecError> {
self.encoder
.set_bitrate(Bitrate::Bits(params.bitrate))
.map_err(|e| CodecError::Init(format!("set_bitrate: {}", e)))?;
self.encoder
.set_inband_fec(params.inband_fec)
.map_err(|e| CodecError::Init(format!("set_inband_fec: {}", e)))?;
self.encoder
.set_packet_loss_perc(params.packet_loss_perc)
.map_err(|e| CodecError::Init(format!("set_packet_loss_perc: {}", e)))?;
self.encoder
.set_dtx(params.dtx)
.map_err(|e| CodecError::Init(format!("set_dtx: {}", e)))?;
Ok(())
}
/// Apply a named [`AudioProfile`] (shorthand for `apply_params(&opus_params(p))`).
pub fn apply_profile(&mut self, profile: AudioProfile) -> Result<(), CodecError> {
self.apply_params(&opus_params(profile))
}
}
impl AudioEncoder for OpusEncoder {
@@ -101,6 +168,47 @@ impl AudioDecoder for OpusDecoder {
mod tests {
use super::*;
#[test]
fn test_opus_params_mapping() {
let low = opus_params(AudioProfile::LowLatency);
let bal = opus_params(AudioProfile::Balanced);
let bad = opus_params(AudioProfile::BadNetwork);
// LowLatency has no loss redundancy; the other two do.
assert!(!low.inband_fec);
assert_eq!(low.packet_loss_perc, 0);
assert!(bal.inband_fec);
assert!(bad.inband_fec);
// BadNetwork is the only profile that enables DTX, and it expects the
// heaviest loss.
assert!(bad.dtx);
assert!(!low.dtx && !bal.dtx);
assert!(bad.packet_loss_perc > bal.packet_loss_perc);
// BadNetwork trims base bitrate to make room for FEC redundancy.
assert!(bad.bitrate < bal.bitrate);
// All bitrates are sane positive voice rates.
for p in [low, bal, bad] {
assert!(p.bitrate > 0 && p.bitrate <= 64_000);
assert!((0..=100).contains(&p.packet_loss_perc));
}
}
#[test]
fn test_apply_profile_sets_bitrate() {
let mut encoder = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
// Every profile applies cleanly to a real encoder...
for profile in AudioProfile::ALL {
encoder.apply_profile(profile).unwrap();
}
// ...and the last-applied bitrate is reflected by the encoder.
encoder.apply_profile(AudioProfile::Balanced).unwrap();
let want = opus_params(AudioProfile::Balanced).bitrate;
assert_eq!(encoder.encoder.get_bitrate().unwrap(), Bitrate::Bits(want));
}
#[test]
fn test_round_trip() {
let mut encoder = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();