feat: Phase 3 — crypto handshake, codec2, benchmarks, audio I/O, relay forwarding
E2E crypto handshake: - Client/relay handshake via SignalMessage (CallOffer/CallAnswer) - X25519 ephemeral key exchange with Ed25519 identity signatures - Integration tests proving bidirectional encrypt/decrypt Codec2 integration: - Pure Rust codec2 crate (v0.3) — no C bindings needed - MODE_3200 (160 samples/20ms, 8 bytes) and MODE_1200 (320 samples/40ms, 6 bytes) - 11 new tests including encode/decode roundtrip and adaptive switching Relay forwarding: - Bidirectional client → remote forwarding with pipeline processing - CLI args: --listen, --remote - Periodic stats logging, clean shutdown via tokio::select! Benchmark tool (wzp-bench): - Codec roundtrip, FEC recovery, crypto throughput, full pipeline benchmarks - Sine wave PCM generator for realistic testing Audio I/O (cpal): - AudioCapture (microphone) and AudioPlayback (speakers) at 48kHz mono - CLI --live mode: mic → encode → send / recv → decode → speakers 120 tests passing, 0 failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,26 +1,38 @@
|
||||
//! Codec2 encoder — stub implementation.
|
||||
//! Codec2 encoder — real implementation via the pure-Rust `codec2` crate.
|
||||
//!
|
||||
//! Codec2 operates at 8 kHz mono. Resampling from 48 kHz is handled
|
||||
//! externally (see `resample.rs` and `AdaptiveCodec`).
|
||||
//!
|
||||
//! This is a stub that returns an error on encode. When `codec2-sys`
|
||||
//! is linked, replace the body of `encode()` with actual FFI calls.
|
||||
|
||||
use codec2::{Codec2 as C2, Codec2Mode};
|
||||
use wzp_proto::{AudioEncoder, CodecError, CodecId, QualityProfile};
|
||||
|
||||
/// Stub Codec2 encoder implementing `AudioEncoder`.
|
||||
/// Maps our `CodecId` to the `codec2` crate's `Codec2Mode`.
|
||||
fn mode_for(codec: CodecId) -> Result<Codec2Mode, CodecError> {
|
||||
match codec {
|
||||
CodecId::Codec2_3200 => Ok(Codec2Mode::MODE_3200),
|
||||
CodecId::Codec2_1200 => Ok(Codec2Mode::MODE_1200),
|
||||
other => Err(CodecError::EncodeFailed(format!(
|
||||
"not a Codec2 variant: {other:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Codec2 encoder implementing `AudioEncoder`.
|
||||
///
|
||||
/// Currently returns `CodecError::EncodeFailed` for all encode operations.
|
||||
/// The structure is ready for drop-in replacement once `codec2-sys` is available.
|
||||
/// Wraps the pure-Rust `codec2` crate. Input is 8 kHz mono i16 PCM;
|
||||
/// the `AdaptiveEncoder` handles 48 kHz -> 8 kHz resampling.
|
||||
pub struct Codec2Encoder {
|
||||
inner: C2,
|
||||
codec_id: CodecId,
|
||||
frame_duration_ms: u8,
|
||||
}
|
||||
|
||||
impl Codec2Encoder {
|
||||
/// Create a new stub Codec2 encoder.
|
||||
/// Create a new Codec2 encoder for the given quality profile.
|
||||
pub fn new(profile: QualityProfile) -> Result<Self, CodecError> {
|
||||
let mode = mode_for(profile.codec)?;
|
||||
Ok(Self {
|
||||
inner: C2::new(mode),
|
||||
codec_id: profile.codec,
|
||||
frame_duration_ms: profile.frame_duration_ms,
|
||||
})
|
||||
@@ -28,15 +40,35 @@ impl Codec2Encoder {
|
||||
|
||||
/// Expected number of 8 kHz PCM samples per frame.
|
||||
pub fn frame_samples(&self) -> usize {
|
||||
(8_000 * self.frame_duration_ms as usize) / 1000
|
||||
self.inner.samples_per_frame()
|
||||
}
|
||||
|
||||
/// Number of compressed bytes per frame.
|
||||
fn bytes_per_frame(&self) -> usize {
|
||||
(self.inner.bits_per_frame() + 7) / 8
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioEncoder for Codec2Encoder {
|
||||
fn encode(&mut self, _pcm: &[i16], _out: &mut [u8]) -> Result<usize, CodecError> {
|
||||
Err(CodecError::EncodeFailed(
|
||||
"codec2-sys not yet linked".to_string(),
|
||||
))
|
||||
fn encode(&mut self, pcm: &[i16], out: &mut [u8]) -> Result<usize, CodecError> {
|
||||
let spf = self.inner.samples_per_frame();
|
||||
let bpf = self.bytes_per_frame();
|
||||
|
||||
if pcm.len() < spf {
|
||||
return Err(CodecError::EncodeFailed(format!(
|
||||
"need {spf} samples, got {}",
|
||||
pcm.len()
|
||||
)));
|
||||
}
|
||||
if out.len() < bpf {
|
||||
return Err(CodecError::EncodeFailed(format!(
|
||||
"output buffer too small: need {bpf} bytes, got {}",
|
||||
out.len()
|
||||
)));
|
||||
}
|
||||
|
||||
self.inner.encode(&mut out[..bpf], &pcm[..spf]);
|
||||
Ok(bpf)
|
||||
}
|
||||
|
||||
fn codec_id(&self) -> CodecId {
|
||||
@@ -46,6 +78,11 @@ impl AudioEncoder for Codec2Encoder {
|
||||
fn set_profile(&mut self, profile: QualityProfile) -> Result<(), CodecError> {
|
||||
match profile.codec {
|
||||
CodecId::Codec2_3200 | CodecId::Codec2_1200 => {
|
||||
// Recreate the inner encoder if the mode changed.
|
||||
if profile.codec != self.codec_id {
|
||||
let mode = mode_for(profile.codec)?;
|
||||
self.inner = C2::new(mode);
|
||||
}
|
||||
self.codec_id = profile.codec;
|
||||
self.frame_duration_ms = profile.frame_duration_ms;
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user