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>
153 lines
4.9 KiB
Rust
153 lines
4.9 KiB
Rust
//! Temporal interleaving — spreads symbols from multiple FEC blocks across
|
|
//! transmission slots so that burst losses damage multiple blocks lightly
|
|
//! rather than one block fatally.
|
|
|
|
/// A symbol ready for transmission: (block_id, symbol_index, is_repair, data).
|
|
pub type Symbol = (u8, u8, bool, Vec<u8>);
|
|
|
|
/// Temporal interleaver that mixes symbols across multiple FEC blocks.
|
|
pub struct Interleaver {
|
|
/// Number of blocks to interleave across (spread depth).
|
|
depth: usize,
|
|
}
|
|
|
|
impl Interleaver {
|
|
/// Create an interleaver with the given spread depth.
|
|
pub fn new(depth: usize) -> Self {
|
|
Self { depth }
|
|
}
|
|
|
|
/// Create with default depth of 3 blocks.
|
|
pub fn with_default_depth() -> Self {
|
|
Self::new(3)
|
|
}
|
|
|
|
/// Spread depth (number of blocks mixed together).
|
|
pub fn depth(&self) -> usize {
|
|
self.depth
|
|
}
|
|
|
|
/// Interleave symbols from multiple blocks into a single transmission sequence.
|
|
///
|
|
/// Each inner `Vec` contains the symbols for one FEC block.
|
|
/// The output interleaves them in round-robin fashion: symbol 0 from block A,
|
|
/// symbol 0 from block B, symbol 0 from block C, symbol 1 from block A, etc.
|
|
///
|
|
/// This ensures a burst loss of N consecutive packets only destroys at most
|
|
/// ceil(N/depth) symbols from any single block.
|
|
pub fn interleave(&self, blocks: &[Vec<Symbol>]) -> Vec<Symbol> {
|
|
if blocks.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
|
|
let max_len = blocks.iter().map(|b| b.len()).max().unwrap_or(0);
|
|
let mut output = Vec::with_capacity(blocks.iter().map(|b| b.len()).sum());
|
|
|
|
for slot in 0..max_len {
|
|
for block in blocks {
|
|
if slot < block.len() {
|
|
output.push(block[slot].clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
output
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn interleave_mixes_blocks() {
|
|
let interleaver = Interleaver::with_default_depth();
|
|
|
|
let block_a: Vec<Symbol> = (0..3)
|
|
.map(|i| (0u8, i as u8, false, vec![0xA0 + i as u8]))
|
|
.collect();
|
|
let block_b: Vec<Symbol> = (0..3)
|
|
.map(|i| (1u8, i as u8, false, vec![0xB0 + i as u8]))
|
|
.collect();
|
|
let block_c: Vec<Symbol> = (0..3)
|
|
.map(|i| (2u8, i as u8, false, vec![0xC0 + i as u8]))
|
|
.collect();
|
|
|
|
let result = interleaver.interleave(&[block_a, block_b, block_c]);
|
|
|
|
assert_eq!(result.len(), 9);
|
|
|
|
// Round-robin: A0, B0, C0, A1, B1, C1, A2, B2, C2
|
|
assert_eq!(result[0].0, 0); // block A
|
|
assert_eq!(result[1].0, 1); // block B
|
|
assert_eq!(result[2].0, 2); // block C
|
|
assert_eq!(result[3].0, 0); // block A
|
|
assert_eq!(result[4].0, 1); // block B
|
|
assert_eq!(result[5].0, 2); // block C
|
|
|
|
// Verify symbol indices cycle correctly
|
|
assert_eq!(result[0].1, 0); // sym 0 from A
|
|
assert_eq!(result[3].1, 1); // sym 1 from A
|
|
assert_eq!(result[6].1, 2); // sym 2 from A
|
|
}
|
|
|
|
#[test]
|
|
fn interleave_unequal_lengths() {
|
|
let interleaver = Interleaver::new(2);
|
|
|
|
let block_a: Vec<Symbol> = (0..3)
|
|
.map(|i| (0u8, i as u8, false, vec![0xA0 + i as u8]))
|
|
.collect();
|
|
let block_b: Vec<Symbol> = (0..1)
|
|
.map(|i| (1u8, i as u8, false, vec![0xB0 + i as u8]))
|
|
.collect();
|
|
|
|
let result = interleaver.interleave(&[block_a, block_b]);
|
|
|
|
// A0, B0, A1, A2
|
|
assert_eq!(result.len(), 4);
|
|
assert_eq!(result[0].0, 0); // A0
|
|
assert_eq!(result[1].0, 1); // B0
|
|
assert_eq!(result[2].0, 0); // A1
|
|
assert_eq!(result[3].0, 0); // A2
|
|
}
|
|
|
|
#[test]
|
|
fn interleave_empty() {
|
|
let interleaver = Interleaver::with_default_depth();
|
|
let result = interleaver.interleave(&[]);
|
|
assert!(result.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn burst_loss_distributed() {
|
|
// With 3-block interleaving and a burst of 6 consecutive losses,
|
|
// each block loses at most 2 symbols.
|
|
let interleaver = Interleaver::new(3);
|
|
|
|
let blocks: Vec<Vec<Symbol>> = (0..3)
|
|
.map(|b| {
|
|
(0..6)
|
|
.map(|i| (b as u8, i as u8, false, vec![b as u8; 10]))
|
|
.collect()
|
|
})
|
|
.collect();
|
|
|
|
let interleaved = interleaver.interleave(&blocks);
|
|
assert_eq!(interleaved.len(), 18);
|
|
|
|
// Simulate burst loss of 6 consecutive packets starting at index 3
|
|
let lost_range = 3..9;
|
|
let mut losses_per_block = [0u32; 3];
|
|
for idx in lost_range {
|
|
let block_id = interleaved[idx].0 as usize;
|
|
losses_per_block[block_id] += 1;
|
|
}
|
|
|
|
// Each block should lose exactly 2 (6 losses / 3 blocks)
|
|
for &loss in &losses_per_block {
|
|
assert_eq!(loss, 2, "Each block should lose at most 2 symbols from a burst of 6");
|
|
}
|
|
}
|
|
}
|