use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, Instant};
use hound::WavReader;
use lac::{decode_frame, encode_frame};
const CORPUS_DIR: &str = "corpus";
const FRAME_SIZE: usize = 4096;
fn load_wav_channels(path: &Path) -> Option<Vec<Vec<i32>>> {
let mut reader = WavReader::open(path).ok()?;
let spec = reader.spec();
if spec.sample_format != hound::SampleFormat::Int {
return None;
}
let ch = spec.channels as usize;
if spec.bits_per_sample > 24 {
return None;
}
let mut channels: Vec<Vec<i32>> = (0..ch).map(|_| Vec::new()).collect();
for (i, s) in reader.samples::<i32>().enumerate() {
let s = s.ok()?;
channels[i % ch].push(s);
}
Some(channels)
}
fn corpus_path(name: &str) -> PathBuf {
Path::new(CORPUS_DIR).join(name)
}
macro_rules! require_corpus {
($path:expr) => {
if !$path.exists() {
eprintln!("skipping: corpus file not found: {}", $path.display());
return;
}
};
}
struct Measurement {
raw_bytes: usize,
encoded_bytes: usize,
encode_time: Duration,
}
impl Measurement {
fn new() -> Self {
Self {
raw_bytes: 0,
encoded_bytes: 0,
encode_time: Duration::ZERO,
}
}
fn add(&mut self, other: &Measurement) {
self.raw_bytes += other.raw_bytes;
self.encoded_bytes += other.encoded_bytes;
self.encode_time += other.encode_time;
}
}
fn roundtrip_channel(channel: &[i32], bytes_per_sample: usize) -> Measurement {
let mut m = Measurement::new();
for chunk in channel.chunks(FRAME_SIZE) {
let t = Instant::now();
let encoded = encode_frame(chunk);
m.encode_time += t.elapsed();
let decoded = decode_frame(&encoded).expect("decode_frame failed on own output");
assert_eq!(
decoded,
chunk,
"round-trip mismatch in frame of {} samples",
chunk.len()
);
m.raw_bytes += chunk.len() * bytes_per_sample;
m.encoded_bytes += encoded.len();
}
m
}
fn roundtrip_wav(path: &Path) -> Measurement {
let spec = WavReader::open(path).expect("open for spec").spec();
let bytes_per_sample = spec.bits_per_sample.div_ceil(8) as usize;
let channels = load_wav_channels(path).expect("load_wav_channels failed");
let mut totals = Measurement::new();
for ch in &channels {
let m = roundtrip_channel(ch, bytes_per_sample);
totals.add(&m);
}
totals
}
fn flac_compress_size(path: &Path) -> Option<usize> {
let out = Command::new("flac")
.arg("--stdout")
.arg("--silent")
.arg("--best")
.arg(path)
.output()
.ok()?;
if !out.status.success() {
return None;
}
Some(out.stdout.len())
}
fn report_ratio(name: &str, m: &Measurement, flac_size: Option<usize>) {
let ratio = m.encoded_bytes as f64 / m.raw_bytes as f64;
let enc_ms = m.encode_time.as_secs_f64() * 1000.0;
eprint!(
"{name:40} raw={:>10} lac={:>10} ratio={ratio:.3} lac_enc_ms={enc_ms:>7.1}",
m.raw_bytes, m.encoded_bytes,
);
if let Some(flac) = flac_size {
let flac_ratio = flac as f64 / m.raw_bytes as f64;
let lac_vs_flac = m.encoded_bytes as f64 / flac as f64;
eprint!(" flac={flac:>10} flac_ratio={flac_ratio:.3} lac/flac={lac_vs_flac:.3}");
}
eprintln!();
}
const GOLDBERG_PREFIX: &str =
"Kimiko Ishizaka - J.S. Bach- -Open- Goldberg Variations, BWV 988 (Piano)";
#[test]
fn bach_aria() {
let path = corpus_path(&format!("{GOLDBERG_PREFIX} - 01 Aria.wav"));
require_corpus!(path);
let m = roundtrip_wav(&path);
let flac = flac_compress_size(&path);
report_ratio("bach_aria (solo piano, tonal)", &m, flac);
let ratio = m.encoded_bytes as f64 / m.raw_bytes as f64;
assert!(
ratio < 0.503,
"bach_aria ratio {} exceeds regression ceiling 0.503",
ratio
);
}
#[test]
fn bach_variatio_4_fughetta() {
let path = corpus_path(&format!("{GOLDBERG_PREFIX} - 05 Variatio 4 a 1 Clav..wav"));
require_corpus!(path);
let m = roundtrip_wav(&path);
let flac = flac_compress_size(&path);
report_ratio("bach_variatio_4 (fugal)", &m, flac);
let ratio = m.encoded_bytes as f64 / m.raw_bytes as f64;
assert!(
ratio < 0.534,
"bach_variatio_4 ratio {} exceeds regression ceiling 0.534",
ratio
);
}
#[test]
fn bach_variatio_16_ouverture() {
let path = corpus_path(&format!(
"{GOLDBERG_PREFIX} - 17 Variatio 16 a 1 Clav. Ouverture.wav"
));
require_corpus!(path);
let m = roundtrip_wav(&path);
let flac = flac_compress_size(&path);
report_ratio("bach_variatio_16 (ouverture)", &m, flac);
let ratio = m.encoded_bytes as f64 / m.raw_bytes as f64;
assert!(
ratio < 0.532,
"bach_variatio_16 ratio {} exceeds regression ceiling 0.532",
ratio
);
}
#[test]
fn ami_headset_speech() {
let path = corpus_path("ES2002a.Headset-0.wav");
require_corpus!(path);
let m = roundtrip_wav(&path);
let flac = flac_compress_size(&path);
report_ratio("ami_headset_speech", &m, flac);
let ratio = m.encoded_bytes as f64 / m.raw_bytes as f64;
assert!(
ratio < 0.195,
"headset ratio {} exceeds regression ceiling 0.195",
ratio
);
}
#[test]
fn ami_array_speech() {
let path = corpus_path("ES2002a.Array1-01.wav");
require_corpus!(path);
let m = roundtrip_wav(&path);
let flac = flac_compress_size(&path);
report_ratio("ami_array_speech", &m, flac);
let ratio = m.encoded_bytes as f64 / m.raw_bytes as f64;
assert!(
ratio < 0.395,
"array speech ratio {} exceeds regression ceiling 0.395",
ratio
);
}
#[test]
fn ami_mixed_meeting() {
let path = corpus_path("ES2002a.Mix-Headset.wav");
require_corpus!(path);
let m = roundtrip_wav(&path);
let flac = flac_compress_size(&path);
report_ratio("ami_mixed_meeting", &m, flac);
let ratio = m.encoded_bytes as f64 / m.raw_bytes as f64;
assert!(
ratio < 0.312,
"mixed meeting ratio {} exceeds regression ceiling 0.312",
ratio
);
}