feat: --send-file for real audio testing + fix warnings

- --send-file <file.raw> sends a raw PCM file (48kHz mono s16le) through relay
- Combine with --record: --send-file talk.raw --record echo.raw <relay>
- Fixed all unused import warnings in echo_test.rs

Convert any audio to test format:
  ffmpeg -i input.mp3 -ar 48000 -ac 1 -f s16le input.raw

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-27 17:51:55 +04:00
parent 28d5a3a9ad
commit d5390db7af
2 changed files with 45 additions and 20 deletions

View File

@@ -37,6 +37,7 @@ struct CliArgs {
relay_addr: SocketAddr,
live: bool,
send_tone_secs: Option<u32>,
send_file: Option<String>,
record_file: Option<String>,
echo_test_secs: Option<u32>,
}
@@ -45,6 +46,7 @@ fn parse_args() -> CliArgs {
let args: Vec<String> = std::env::args().collect();
let mut live = false;
let mut send_tone_secs = None;
let mut send_file = None;
let mut record_file = None;
let mut echo_test_secs = None;
let mut relay_str = None;
@@ -62,6 +64,14 @@ fn parse_args() -> CliArgs {
.expect("--send-tone value must be a number"),
);
}
"--send-file" => {
i += 1;
send_file = Some(
args.get(i)
.expect("--send-file requires a filename")
.to_string(),
);
}
"--record" => {
i += 1;
record_file = Some(
@@ -85,6 +95,7 @@ fn parse_args() -> CliArgs {
eprintln!("Options:");
eprintln!(" --live Live mic/speaker mode");
eprintln!(" --send-tone <secs> Send a 440Hz test tone for N seconds");
eprintln!(" --send-file <file> Send a raw PCM file (48kHz mono s16le)");
eprintln!(" --record <file.raw> Record received audio to raw PCM file");
eprintln!(" --echo-test <secs> Run automated echo quality test");
eprintln!(" (48kHz mono s16le, play with ffplay -f s16le -ar 48000 -ch_layout mono file.raw)");
@@ -113,6 +124,7 @@ fn parse_args() -> CliArgs {
relay_addr,
live,
send_tone_secs,
send_file,
record_file,
echo_test_secs,
}
@@ -160,8 +172,8 @@ async fn main() -> anyhow::Result<()> {
wzp_client::echo_test::print_report(&result);
transport.close().await?;
Ok(())
} else if cli.send_tone_secs.is_some() || cli.record_file.is_some() {
run_file_mode(transport, cli.send_tone_secs, cli.record_file).await
} else if cli.send_tone_secs.is_some() || cli.send_file.is_some() || cli.record_file.is_some() {
run_file_mode(transport, cli.send_tone_secs, cli.send_file, cli.record_file).await
} else {
run_silence(transport).await
}
@@ -210,37 +222,53 @@ async fn run_silence(transport: Arc<wzp_transport::QuinnTransport>) -> anyhow::R
Ok(())
}
/// File/tone mode: send a test tone and/or record received audio.
/// File/tone mode: send a test tone or audio file, and/or record received audio.
async fn run_file_mode(
transport: Arc<wzp_transport::QuinnTransport>,
send_tone_secs: Option<u32>,
send_file: Option<String>,
record_file: Option<String>,
) -> anyhow::Result<()> {
let config = CallConfig::default();
// --- Send task: generate tone and send ---
// --- Send task: generate tone or play file ---
let send_transport = transport.clone();
let send_handle = tokio::spawn(async move {
let secs = match send_tone_secs {
Some(s) => s,
None => {
// No sending, just wait
tokio::signal::ctrl_c().await.ok();
return;
}
// Load PCM frames from file or generate tone
let pcm_frames: Vec<Vec<i16>> = if let Some(ref path) = send_file {
// Read raw PCM file (48kHz mono s16le)
let bytes = match std::fs::read(path) {
Ok(b) => b,
Err(e) => { error!("read {path}: {e}"); return; }
};
let samples: Vec<i16> = bytes.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect();
let duration = samples.len() as f64 / 48_000.0;
info!(file = %path, duration = format!("{:.1}s", duration), "sending audio file");
samples.chunks(FRAME_SAMPLES)
.filter(|c| c.len() == FRAME_SAMPLES)
.map(|c| c.to_vec())
.collect()
} else if let Some(secs) = send_tone_secs {
let total = (secs as u64) * 50;
info!(seconds = secs, frames = total, "sending 440Hz tone");
(0..total).map(|i| generate_sine_frame(440.0, 48_000, i)).collect()
} else {
// No sending, just wait
tokio::signal::ctrl_c().await.ok();
return;
};
let mut encoder = CallEncoder::new(&config);
let total_frames = (secs as u64) * 50; // 50 frames/sec at 20ms
let total_frames = pcm_frames.len() as u64;
let frame_duration = tokio::time::Duration::from_millis(20);
let mut total_source = 0u64;
let mut total_repair = 0u64;
info!(seconds = secs, frames = total_frames, "sending 440Hz tone");
for frame_idx in 0..total_frames {
let pcm = generate_sine_frame(440.0, 48_000, frame_idx);
for (frame_idx, pcm) in pcm_frames.iter().enumerate() {
let frame_idx = frame_idx as u64;
let packets = match encoder.encode_frame(&pcm) {
Ok(p) => p,
Err(e) => {

View File

@@ -6,11 +6,8 @@
use std::time::{Duration, Instant};
use bytes::Bytes;
use tracing::{debug, info, warn};
use tracing::info;
use wzp_proto::packet::{MediaHeader, MediaPacket};
use wzp_proto::traits::{AudioDecoder, AudioEncoder, FecDecoder, FecEncoder};
use wzp_proto::MediaTransport;
use crate::call::{CallConfig, CallDecoder, CallEncoder};