Rust workspace with 7 crates implementing a custom VoIP protocol designed for extremely lossy connections (5-70% loss, 100-500kbps, 300-800ms RTT). 89 tests passing across all crates. Crates: - wzp-proto: Wire format, traits, adaptive quality controller, jitter buffer, session FSM - wzp-codec: Opus encoder/decoder (audiopus), Codec2 stubs, adaptive switching, resampling - wzp-fec: RaptorQ fountain codes, interleaving, block management (proven 30-70% loss recovery) - wzp-crypto: X25519+ChaCha20-Poly1305, Warzone identity compatible, anti-replay, rekeying - wzp-transport: QUIC via quinn with DATAGRAM frames, path monitoring, signaling streams - wzp-relay: Integration stub (Phase 2) - wzp-client: Integration stub (Phase 2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
83 lines
2.4 KiB
Rust
83 lines
2.4 KiB
Rust
//! Simple linear resampler for 48 kHz <-> 8 kHz conversion.
|
|
//!
|
|
//! These are basic implementations suitable for voice. For higher quality,
|
|
//! replace with the `rubato` crate later.
|
|
|
|
/// Downsample from 48 kHz to 8 kHz (6:1 decimation with averaging).
|
|
///
|
|
/// Each output sample is the average of 6 consecutive input samples,
|
|
/// providing basic anti-aliasing via a box filter.
|
|
pub fn resample_48k_to_8k(input: &[i16]) -> Vec<i16> {
|
|
const RATIO: usize = 6;
|
|
let out_len = input.len() / RATIO;
|
|
let mut output = Vec::with_capacity(out_len);
|
|
|
|
for chunk in input.chunks_exact(RATIO) {
|
|
let sum: i32 = chunk.iter().map(|&s| s as i32).sum();
|
|
output.push((sum / RATIO as i32) as i16);
|
|
}
|
|
|
|
output
|
|
}
|
|
|
|
/// Upsample from 8 kHz to 48 kHz (1:6 interpolation with linear interp).
|
|
///
|
|
/// Linearly interpolates between each pair of input samples to produce
|
|
/// 6 output samples per input sample.
|
|
pub fn resample_8k_to_48k(input: &[i16]) -> Vec<i16> {
|
|
const RATIO: usize = 6;
|
|
if input.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
|
|
let out_len = input.len() * RATIO;
|
|
let mut output = Vec::with_capacity(out_len);
|
|
|
|
for i in 0..input.len() {
|
|
let current = input[i] as i32;
|
|
let next = if i + 1 < input.len() {
|
|
input[i + 1] as i32
|
|
} else {
|
|
current // hold last sample
|
|
};
|
|
|
|
for j in 0..RATIO {
|
|
let interp = current + (next - current) * j as i32 / RATIO as i32;
|
|
output.push(interp as i16);
|
|
}
|
|
}
|
|
|
|
output
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn roundtrip_length() {
|
|
// 960 samples at 48kHz (20ms) -> 160 samples at 8kHz -> 960 samples at 48kHz
|
|
let input_48k = vec![0i16; 960];
|
|
let down = resample_48k_to_8k(&input_48k);
|
|
assert_eq!(down.len(), 160);
|
|
let up = resample_8k_to_48k(&down);
|
|
assert_eq!(up.len(), 960);
|
|
}
|
|
|
|
#[test]
|
|
fn dc_signal_preserved() {
|
|
// A constant signal should survive resampling
|
|
let input = vec![1000i16; 960];
|
|
let down = resample_48k_to_8k(&input);
|
|
assert!(down.iter().all(|&s| s == 1000));
|
|
let up = resample_8k_to_48k(&down);
|
|
assert!(up.iter().all(|&s| s == 1000));
|
|
}
|
|
|
|
#[test]
|
|
fn empty_input() {
|
|
assert!(resample_48k_to_8k(&[]).is_empty());
|
|
assert!(resample_8k_to_48k(&[]).is_empty());
|
|
}
|
|
}
|