fix: dynamic frame sizing for non-default quality profiles on Android
Some checks failed
Mirror to GitHub / mirror (push) Failing after 36s
Build Release Binaries / build-amd64 (push) Failing after 1m58s

The send loop was hardcoded to 960 samples (20ms/Opus24k), causing
DEGRADED (Opus 6k, 40ms) and CATASTROPHIC (Codec2 1200, 40ms) to
fail — the encoder needed 1920 samples but only got 960.

Changes:
- capture_buf, ring read threshold, and timestamp increment are now
  computed from profile.frame_duration_ms (960 for 20ms, 1920 for 40ms)
- decode_buf sized to MAX_FRAME_SAMPLES (1920) to handle any incoming codec
- recv codec switch now uses correct QualityProfile per codec (was
  inheriting original profile's frame_duration_ms, breaking cross-codec)
- added ComfortNoise guard on recv path

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-07 18:00:27 +04:00
parent 68b56d9172
commit fa3c7f1cef

View File

@@ -27,8 +27,13 @@ use crate::audio_ring::AudioRing;
use crate::commands::EngineCommand; use crate::commands::EngineCommand;
use crate::stats::{CallState, CallStats}; use crate::stats::{CallState, CallStats};
/// Opus frame size at 48kHz mono, 20ms = 960 samples. /// Max frame size at 48kHz mono (40ms = 1920 samples, for Codec2/Opus6k).
const FRAME_SAMPLES: usize = 960; const MAX_FRAME_SAMPLES: usize = 1920;
/// Compute frame samples at 48kHz for a given profile.
fn frame_samples_for(profile: &QualityProfile) -> usize {
(profile.frame_duration_ms as usize) * 48 // 48000 / 1000
}
/// Configuration to start a call. /// Configuration to start a call.
pub struct CallStartConfig { pub struct CallStartConfig {
@@ -350,18 +355,22 @@ async fn run_call(
let mut capture_agc = AutoGainControl::new(); let mut capture_agc = AutoGainControl::new();
let mut playout_agc = AutoGainControl::new(); let mut playout_agc = AutoGainControl::new();
let frame_samples = frame_samples_for(&profile);
info!( info!(
codec = ?profile.codec,
fec_ratio = profile.fec_ratio, fec_ratio = profile.fec_ratio,
frames_per_block = profile.frames_per_block, frames_per_block = profile.frames_per_block,
"codec + FEC + AGC initialized (48kHz mono, 20ms frames)" frame_ms = profile.frame_duration_ms,
frame_samples,
"codec + FEC + AGC initialized"
); );
let seq = AtomicU16::new(0); let seq = AtomicU16::new(0);
let ts = AtomicU32::new(0); let ts = AtomicU32::new(0);
let transport_recv = transport.clone(); let transport_recv = transport.clone();
// Pre-allocate buffers // Pre-allocate buffers (sized for current profile)
let mut capture_buf = vec![0i16; FRAME_SAMPLES]; let mut capture_buf = vec![0i16; frame_samples];
let mut encode_buf = vec![0u8; encoder.max_frame_bytes()]; let mut encode_buf = vec![0u8; encoder.max_frame_bytes()];
let mut frame_in_block: u8 = 0; let mut frame_in_block: u8 = 0;
let mut block_id: u8 = 0; let mut block_id: u8 = 0;
@@ -391,13 +400,13 @@ async fn run_call(
} }
let avail = state.capture_ring.available(); let avail = state.capture_ring.available();
if avail < FRAME_SAMPLES { if avail < frame_samples {
tokio::time::sleep(std::time::Duration::from_millis(5)).await; tokio::time::sleep(std::time::Duration::from_millis(5)).await;
continue; continue;
} }
let read = state.capture_ring.read(&mut capture_buf); let read = state.capture_ring.read(&mut capture_buf);
if read < FRAME_SAMPLES { if read < frame_samples {
continue; continue;
} }
@@ -426,7 +435,7 @@ async fn run_call(
// Build source packet // Build source packet
let s = seq.fetch_add(1, Ordering::Relaxed); let s = seq.fetch_add(1, Ordering::Relaxed);
let t = ts.fetch_add(FRAME_SAMPLES as u32, Ordering::Relaxed); let t = ts.fetch_add(frame_samples as u32, Ordering::Relaxed);
let source_pkt = MediaPacket { let source_pkt = MediaPacket {
header: MediaHeader { header: MediaHeader {
@@ -554,8 +563,8 @@ async fn run_call(
info!(frames_sent, frames_dropped, send_errors, "send task ended"); info!(frames_sent, frames_dropped, send_errors, "send task ended");
}; };
// Pre-allocate decode buffer // Pre-allocate decode buffer (max size to handle any incoming codec)
let mut decode_buf = vec![0i16; FRAME_SAMPLES]; let mut decode_buf = vec![0i16; MAX_FRAME_SAMPLES];
// Recv task: MediaPackets → FEC decode → Opus decode → playout ring // Recv task: MediaPackets → FEC decode → Opus decode → playout ring
let recv_task = async { let recv_task = async {
@@ -600,13 +609,22 @@ async fn run_call(
); );
// Source packets: decode directly // Source packets: decode directly
if !is_repair { if !is_repair && pkt.header.codec_id != CodecId::ComfortNoise {
// Switch decoder to match incoming codec if different // Switch decoder to match incoming codec if different
if pkt.header.codec_id != decoder.codec_id() { if pkt.header.codec_id != decoder.codec_id() {
let switch_profile = QualityProfile { let switch_profile = match pkt.header.codec_id {
codec: pkt.header.codec_id, CodecId::Opus24k => QualityProfile::GOOD,
..profile CodecId::Opus6k => QualityProfile::DEGRADED,
CodecId::Codec2_1200 => QualityProfile::CATASTROPHIC,
CodecId::Codec2_3200 => QualityProfile {
codec: CodecId::Codec2_3200,
fec_ratio: 0.5,
frame_duration_ms: 20,
frames_per_block: 5,
},
other => QualityProfile { codec: other, ..QualityProfile::GOOD },
}; };
info!(from = ?decoder.codec_id(), to = ?pkt.header.codec_id, "recv: switching decoder");
let _ = decoder.set_profile(switch_profile); let _ = decoder.set_profile(switch_profile);
} }
match decoder.decode(&pkt.payload, &mut decode_buf) { match decoder.decode(&pkt.payload, &mut decode_buf) {