fix: dynamic frame sizing for non-default quality profiles on Android
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:
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user