Files
wz-phone/crates/wzp-android/src/pipeline.rs
Claude e7b1c3372a feat: Android VoIP client — Phase 2 (JNI bridge, Compose UI, AEC pipeline wiring)
- JNI bridge with 8 extern functions (init, startCall, stopCall, setMute,
  setSpeaker, getStats, forceProfile, destroy) with panic catching
- Kotlin engine layer: WzpEngine JNI wrapper, WzpCallback interface,
  CallStats data class with JSON deserialization
- Jetpack Compose UI: InCallScreen with quality indicator (green/yellow/red),
  mute/speaker/hangup buttons, stats overlay, duration timer
- CallActivity with RECORD_AUDIO permission handling, Material3 theme
- CallService foreground service with WakeLock, WiFi lock, notification
- AudioRouteManager for speaker/earpiece/Bluetooth SCO switching
- AEC wired into CallEncoder pipeline: AEC → AGC → denoise → silence → encode
- AEC farend reference fed from decode path to encode path in pipeline
- Engine exposes set_aec_enabled/set_agc_enabled via AtomicBool flags

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 18:16:38 +00:00

263 lines
8.9 KiB
Rust

//! Codec pipeline — encode/decode with FEC and jitter buffer.
//!
//! Runs on a dedicated thread, processing 20 ms frames at 48 kHz.
//! The pipeline is NOT Send/Sync (Opus encoder state) — it is owned
//! exclusively by the codec thread.
use tracing::{debug, warn};
use wzp_codec::{AdaptiveDecoder, AdaptiveEncoder, AutoGainControl, EchoCanceller};
use wzp_fec::{RaptorQFecDecoder, RaptorQFecEncoder};
use wzp_proto::jitter::{JitterBuffer, PlayoutResult};
use wzp_proto::quality::AdaptiveQualityController;
use wzp_proto::traits::{AudioDecoder, AudioEncoder, FecDecoder, FecEncoder};
use wzp_proto::traits::QualityController;
use wzp_proto::{MediaPacket, QualityProfile};
use crate::audio_android::FRAME_SAMPLES;
/// Maximum encoded frame size (Opus worst case at highest bitrate).
const MAX_ENCODED_BYTES: usize = 1275;
/// Pipeline statistics snapshot.
#[derive(Clone, Debug, Default)]
pub struct PipelineStats {
pub frames_encoded: u64,
pub frames_decoded: u64,
pub underruns: u64,
pub jitter_depth: usize,
pub quality_tier: u8,
}
/// The codec pipeline: encode, FEC, jitter buffer, decode.
///
/// This struct is owned by the codec thread and not shared.
pub struct Pipeline {
encoder: AdaptiveEncoder,
decoder: AdaptiveDecoder,
fec_encoder: RaptorQFecEncoder,
fec_decoder: RaptorQFecDecoder,
jitter_buffer: JitterBuffer,
quality_ctrl: AdaptiveQualityController,
/// Acoustic echo canceller applied before encoding.
aec: EchoCanceller,
/// Automatic gain control applied before encoding.
agc: AutoGainControl,
/// Last decoded PCM frame, used as the AEC far-end reference.
last_decoded_farend: Option<Vec<i16>>,
// Pre-allocated scratch buffers
capture_buf: Vec<i16>,
#[allow(dead_code)]
playout_buf: Vec<i16>,
encode_out: Vec<u8>,
// Stats counters
frames_encoded: u64,
frames_decoded: u64,
underruns: u64,
}
impl Pipeline {
/// Create a new pipeline configured for the given quality profile.
pub fn new(profile: QualityProfile) -> Result<Self, anyhow::Error> {
let encoder = AdaptiveEncoder::new(profile)
.map_err(|e| anyhow::anyhow!("encoder init: {e}"))?;
let decoder = AdaptiveDecoder::new(profile)
.map_err(|e| anyhow::anyhow!("decoder init: {e}"))?;
let fec_encoder =
RaptorQFecEncoder::with_defaults(profile.frames_per_block as usize);
let fec_decoder =
RaptorQFecDecoder::with_defaults(profile.frames_per_block as usize);
let jitter_buffer = JitterBuffer::new(10, 250, 3);
let quality_ctrl = AdaptiveQualityController::new();
Ok(Self {
encoder,
decoder,
fec_encoder,
fec_decoder,
jitter_buffer,
quality_ctrl,
aec: EchoCanceller::new(48000, 100), // 100 ms echo tail
agc: AutoGainControl::new(),
last_decoded_farend: None,
capture_buf: vec![0i16; FRAME_SAMPLES],
playout_buf: vec![0i16; FRAME_SAMPLES],
encode_out: vec![0u8; MAX_ENCODED_BYTES],
frames_encoded: 0,
frames_decoded: 0,
underruns: 0,
})
}
/// Encode a PCM frame into a compressed packet.
///
/// If `muted` is true, a silence frame is encoded (all zeros).
/// Returns the encoded bytes, or `None` on encoder error.
pub fn encode_frame(&mut self, pcm: &[i16], muted: bool) -> Option<Vec<u8>> {
let input = if muted {
// Zero the capture buffer for silence
for s in self.capture_buf.iter_mut() {
*s = 0;
}
&self.capture_buf[..]
} else {
// Feed the last decoded playout as AEC far-end reference.
if let Some(ref farend) = self.last_decoded_farend {
self.aec.feed_farend(farend);
}
// Apply AEC + AGC to the captured PCM.
let len = pcm.len().min(self.capture_buf.len());
self.capture_buf[..len].copy_from_slice(&pcm[..len]);
self.aec.process_frame(&mut self.capture_buf[..len]);
self.agc.process_frame(&mut self.capture_buf[..len]);
&self.capture_buf[..len]
};
match self.encoder.encode(input, &mut self.encode_out) {
Ok(n) => {
self.frames_encoded += 1;
let encoded = self.encode_out[..n].to_vec();
// Feed into FEC encoder
if let Err(e) = self.fec_encoder.add_source_symbol(&encoded) {
warn!("FEC encode error: {e}");
}
Some(encoded)
}
Err(e) => {
warn!("encode error: {e}");
None
}
}
}
/// Feed a received media packet into the jitter buffer.
pub fn feed_packet(&mut self, packet: MediaPacket) {
// Feed FEC symbols if present
let header = &packet.header;
if header.fec_block != 0 || header.fec_symbol != 0 {
let is_repair = header.is_repair;
if let Err(e) = self.fec_decoder.add_symbol(
header.fec_block,
header.fec_symbol,
is_repair,
&packet.payload,
) {
debug!("FEC symbol feed error: {e}");
}
}
self.jitter_buffer.push(packet);
}
/// Decode the next frame from the jitter buffer.
///
/// Returns decoded PCM samples, or `None` if the buffer is not ready.
/// Decoded PCM is also stored as the AEC far-end reference for the next
/// encode cycle.
pub fn decode_frame(&mut self) -> Option<Vec<i16>> {
let result = match self.jitter_buffer.pop() {
PlayoutResult::Packet(pkt) => {
let mut pcm = vec![0i16; FRAME_SAMPLES];
match self.decoder.decode(&pkt.payload, &mut pcm) {
Ok(n) => {
self.frames_decoded += 1;
pcm.truncate(n);
Some(pcm)
}
Err(e) => {
warn!("decode error: {e}");
// Attempt PLC
self.generate_plc()
}
}
}
PlayoutResult::Missing { seq } => {
debug!(seq, "jitter buffer: missing packet, generating PLC");
self.generate_plc()
}
PlayoutResult::NotReady => {
self.underruns += 1;
None
}
};
// Save decoded PCM as far-end reference for AEC.
if let Some(ref pcm) = result {
self.last_decoded_farend = Some(pcm.clone());
}
result
}
/// Generate packet loss concealment output.
fn generate_plc(&mut self) -> Option<Vec<i16>> {
let mut pcm = vec![0i16; FRAME_SAMPLES];
match self.decoder.decode_lost(&mut pcm) {
Ok(n) => {
self.frames_decoded += 1;
pcm.truncate(n);
Some(pcm)
}
Err(e) => {
warn!("PLC error: {e}");
None
}
}
}
/// Feed a quality report into the adaptive quality controller.
///
/// Returns a new profile if a tier transition occurred.
#[allow(unused)]
pub fn observe_quality(
&mut self,
report: &wzp_proto::QualityReport,
) -> Option<QualityProfile> {
let new_profile = self.quality_ctrl.observe(report);
if let Some(ref profile) = new_profile {
if let Err(e) = self.encoder.set_profile(*profile) {
warn!("encoder set_profile error: {e}");
}
if let Err(e) = self.decoder.set_profile(*profile) {
warn!("decoder set_profile error: {e}");
}
}
new_profile
}
/// Force a specific quality profile.
#[allow(unused)]
pub fn force_profile(&mut self, profile: QualityProfile) {
self.quality_ctrl.force_profile(profile);
if let Err(e) = self.encoder.set_profile(profile) {
warn!("encoder set_profile error: {e}");
}
if let Err(e) = self.decoder.set_profile(profile) {
warn!("decoder set_profile error: {e}");
}
}
/// Get current pipeline statistics.
pub fn stats(&self) -> PipelineStats {
PipelineStats {
frames_encoded: self.frames_encoded,
frames_decoded: self.frames_decoded,
underruns: self.underruns,
jitter_depth: self.jitter_buffer.stats().current_depth,
quality_tier: self.quality_ctrl.tier() as u8,
}
}
/// Enable or disable acoustic echo cancellation.
pub fn set_aec_enabled(&mut self, enabled: bool) {
self.aec.set_enabled(enabled);
}
/// Enable or disable automatic gain control.
pub fn set_agc_enabled(&mut self, enabled: bool) {
self.agc.set_enabled(enabled);
}
}