320 lines
12 KiB
Rust
320 lines
12 KiB
Rust
//! Integration tests for `VideoAudioReader` (FFmpeg streaming audio source).
|
|
//!
|
|
//! These build the daw-backend lib in normal mode, so they're independent of
|
|
//! the crate's pre-existing broken `#[cfg(test)]` unit tests (automation.rs).
|
|
//! They synthesize a mono 32-bit-float WAV whose sample `i` has value `i/n`, so
|
|
//! a decoded sample's value identifies its frame index — letting us assert both
|
|
//! in-order decoding and **sample-accurate seeking** (the property video audio
|
|
//! needs to stay synced with other clips).
|
|
|
|
use daw_backend::audio::disk_reader::{
|
|
build_waveform_pyramid, CompressedReader, SourceKind, VideoAudioReader,
|
|
};
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
|
|
fn write_ramp_wav(path: &Path, n: u32, sample_rate: u32) {
|
|
let channels = 1u16;
|
|
let bytes_per_sample = 4u32;
|
|
let data_size = n * bytes_per_sample;
|
|
let mut buf: Vec<u8> = Vec::with_capacity(44 + data_size as usize);
|
|
buf.extend_from_slice(b"RIFF");
|
|
buf.extend_from_slice(&(36 + data_size).to_le_bytes());
|
|
buf.extend_from_slice(b"WAVE");
|
|
buf.extend_from_slice(b"fmt ");
|
|
buf.extend_from_slice(&16u32.to_le_bytes());
|
|
buf.extend_from_slice(&3u16.to_le_bytes()); // IEEE float
|
|
buf.extend_from_slice(&channels.to_le_bytes());
|
|
buf.extend_from_slice(&sample_rate.to_le_bytes());
|
|
buf.extend_from_slice(&(sample_rate * channels as u32 * bytes_per_sample).to_le_bytes());
|
|
buf.extend_from_slice(&((channels as u32 * bytes_per_sample) as u16).to_le_bytes());
|
|
buf.extend_from_slice(&32u16.to_le_bytes());
|
|
buf.extend_from_slice(b"data");
|
|
buf.extend_from_slice(&data_size.to_le_bytes());
|
|
for i in 0..n {
|
|
buf.extend_from_slice(&((i as f32) / (n as f32)).to_le_bytes());
|
|
}
|
|
let mut f = std::fs::File::create(path).unwrap();
|
|
f.write_all(&buf).unwrap();
|
|
}
|
|
|
|
/// Stereo ramp: frame `i` has left = `i/n`, right = `0.5 - i/n` (distinct per
|
|
/// channel), interleaved `[L0,R0,L1,R1,…]`. Exercises the channels>1 path.
|
|
fn write_stereo_ramp_wav(path: &Path, n: u32, sample_rate: u32) {
|
|
let channels = 2u16;
|
|
let bytes_per_sample = 4u32;
|
|
let data_size = n * channels as u32 * bytes_per_sample;
|
|
let mut buf: Vec<u8> = Vec::with_capacity(44 + data_size as usize);
|
|
buf.extend_from_slice(b"RIFF");
|
|
buf.extend_from_slice(&(36 + data_size).to_le_bytes());
|
|
buf.extend_from_slice(b"WAVE");
|
|
buf.extend_from_slice(b"fmt ");
|
|
buf.extend_from_slice(&16u32.to_le_bytes());
|
|
buf.extend_from_slice(&3u16.to_le_bytes()); // IEEE float
|
|
buf.extend_from_slice(&channels.to_le_bytes());
|
|
buf.extend_from_slice(&sample_rate.to_le_bytes());
|
|
buf.extend_from_slice(&(sample_rate * channels as u32 * bytes_per_sample).to_le_bytes());
|
|
buf.extend_from_slice(&((channels as u32 * bytes_per_sample) as u16).to_le_bytes());
|
|
buf.extend_from_slice(&32u16.to_le_bytes());
|
|
buf.extend_from_slice(b"data");
|
|
buf.extend_from_slice(&data_size.to_le_bytes());
|
|
for i in 0..n {
|
|
let l = i as f32 / n as f32;
|
|
let r = 0.5 - i as f32 / n as f32;
|
|
buf.extend_from_slice(&l.to_le_bytes());
|
|
buf.extend_from_slice(&r.to_le_bytes());
|
|
}
|
|
let mut f = std::fs::File::create(path).unwrap();
|
|
f.write_all(&buf).unwrap();
|
|
}
|
|
|
|
fn temp_path(tag: &str) -> std::path::PathBuf {
|
|
let mut p = std::env::temp_dir();
|
|
p.push(format!("lb_videoaudio_test_{}_{}.wav", std::process::id(), tag));
|
|
p
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_samples_in_order() {
|
|
let n = 4000u32;
|
|
let sr = 8000u32;
|
|
let path = temp_path("seq");
|
|
write_ramp_wav(&path, n, sr);
|
|
|
|
let mut reader = VideoAudioReader::open(&path).unwrap();
|
|
assert_eq!(reader.channels(), 1);
|
|
assert_eq!(reader.sample_rate(), sr);
|
|
// Probe estimate (used by add_video_audio_sync) should be ~n frames.
|
|
let tf = reader.total_frames() as f64;
|
|
assert!(
|
|
(tf - n as f64).abs() < n as f64 * 0.1,
|
|
"total_frames {} not ~{}",
|
|
tf, n
|
|
);
|
|
|
|
let mut all = Vec::new();
|
|
let mut buf = Vec::new();
|
|
loop {
|
|
let frames = reader.decode_next(&mut buf).unwrap();
|
|
if frames == 0 {
|
|
break;
|
|
}
|
|
all.extend_from_slice(&buf);
|
|
}
|
|
|
|
// Allow a couple of priming/flush samples of slack at the very end.
|
|
assert!(all.len() + 4 >= n as usize, "decoded too few samples: {}", all.len());
|
|
for (i, &v) in all.iter().enumerate().take(n as usize) {
|
|
let expected = i as f32 / n as f32;
|
|
assert!((v - expected).abs() < 1e-3, "sample {} = {}, expected {}", i, v, expected);
|
|
}
|
|
let _ = std::fs::remove_file(&path);
|
|
}
|
|
|
|
/// CompressedReader (symphonia) must seek **sample-accurately** too, so compressed
|
|
/// audio stays frame-synced with video audio. Symphonia decodes WAV via the same
|
|
/// path; its coarse seek lands on packet boundaries, exercising the decode-discard.
|
|
#[test]
|
|
fn compressed_reader_seek_is_sample_accurate() {
|
|
let n = 4000u32;
|
|
let sr = 8000u32;
|
|
let path = temp_path("comp_seek");
|
|
write_ramp_wav(&path, n, sr);
|
|
|
|
let mut reader = CompressedReader::open(&path).unwrap();
|
|
assert_eq!(reader.channels(), 1);
|
|
assert_eq!(reader.sample_rate(), sr);
|
|
|
|
for &target in &[2000u64, 137, 3500, 0] {
|
|
let actual = reader.seek(target).unwrap();
|
|
assert_eq!(actual, target, "seek should report the exact target");
|
|
|
|
let mut buf = Vec::new();
|
|
let mut frames = 0;
|
|
for _ in 0..128 {
|
|
frames = reader.decode_next(&mut buf).unwrap();
|
|
if frames > 0 {
|
|
break;
|
|
}
|
|
}
|
|
assert!(frames > 0, "no samples after seek to {}", target);
|
|
let expected = target as f32 / n as f32;
|
|
assert!(
|
|
(buf[0] - expected).abs() < 1e-3,
|
|
"compressed seek to {}: first sample = {}, expected {}",
|
|
target, buf[0], expected
|
|
);
|
|
}
|
|
let _ = std::fs::remove_file(&path);
|
|
}
|
|
|
|
/// The decode→pyramid bridge should produce an envelope matching the signal,
|
|
/// through both reader backends (symphonia + ffmpeg), with bounded memory.
|
|
#[test]
|
|
fn waveform_pyramid_from_decode_matches_signal() {
|
|
let n = 5000u32;
|
|
let sr = 8000u32;
|
|
let path = temp_path("pyr");
|
|
write_ramp_wav(&path, n, sr); // ramp 0 .. (n-1)/n, all positive
|
|
|
|
for kind in [SourceKind::CompressedAudio, SourceKind::VideoAudio] {
|
|
let p = build_waveform_pyramid(&path, kind, 256).unwrap();
|
|
assert_eq!(p.channels, 1);
|
|
assert_eq!(p.root().len(), 1, "{:?}: root should be one texel", kind);
|
|
let root = p.root()[0];
|
|
assert!(root.l_min.abs() < 1e-2, "{:?}: root min {} ~ 0", kind, root.l_min);
|
|
let expected_max = (n - 1) as f32 / n as f32;
|
|
assert!(
|
|
(root.l_max - expected_max).abs() < 1e-2,
|
|
"{:?}: root max {} ~ {}", kind, root.l_max, expected_max
|
|
);
|
|
// Frame count is approximate across decoders (priming/resampler overhead);
|
|
// the envelope above is the real check. Just confirm it's about right.
|
|
assert!((p.total_frames as i64 - n as i64).abs() < 128, "{:?}: frames {}", kind, p.total_frames);
|
|
}
|
|
let _ = std::fs::remove_file(&path);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_stereo_interleaved() {
|
|
let n = 2000u32;
|
|
let sr = 8000u32;
|
|
let path = temp_path("stereo");
|
|
write_stereo_ramp_wav(&path, n, sr);
|
|
|
|
let mut reader = VideoAudioReader::open(&path).unwrap();
|
|
assert_eq!(reader.channels(), 2);
|
|
|
|
let mut all = Vec::new();
|
|
let mut buf = Vec::new();
|
|
loop {
|
|
let frames = reader.decode_next(&mut buf).unwrap();
|
|
if frames == 0 {
|
|
break;
|
|
}
|
|
// Each decode_next returns whole interleaved frames.
|
|
assert_eq!(buf.len() % 2, 0, "stereo decode returned a partial frame");
|
|
all.extend_from_slice(&buf);
|
|
}
|
|
|
|
// Interleaved L/R, ~n frames.
|
|
assert!(all.len() + 8 >= (n * 2) as usize, "decoded too few samples: {}", all.len());
|
|
for i in 0..n as usize {
|
|
let l = all[2 * i];
|
|
let r = all[2 * i + 1];
|
|
assert!((l - i as f32 / n as f32).abs() < 1e-3, "L[{}]={} expected {}", i, l, i as f32 / n as f32);
|
|
assert!(
|
|
(r - (0.5 - i as f32 / n as f32)).abs() < 1e-3,
|
|
"R[{}]={} expected {}", i, r, 0.5 - i as f32 / n as f32
|
|
);
|
|
}
|
|
let _ = std::fs::remove_file(&path);
|
|
}
|
|
|
|
#[test]
|
|
fn seek_is_sample_accurate() {
|
|
let n = 4000u32;
|
|
let sr = 8000u32;
|
|
let path = temp_path("seek");
|
|
write_ramp_wav(&path, n, sr);
|
|
|
|
let mut reader = VideoAudioReader::open(&path).unwrap();
|
|
|
|
for &target in &[2000u64, 137, 3500, 0] {
|
|
let actual = reader.seek(target).unwrap();
|
|
assert_eq!(actual, target);
|
|
|
|
// Pull the first non-empty decode after the seek.
|
|
let mut buf = Vec::new();
|
|
let mut frames = 0;
|
|
for _ in 0..64 {
|
|
frames = reader.decode_next(&mut buf).unwrap();
|
|
if frames > 0 {
|
|
break;
|
|
}
|
|
}
|
|
assert!(frames > 0, "no samples after seek to {}", target);
|
|
|
|
let expected = target as f32 / n as f32;
|
|
assert!(
|
|
(buf[0] - expected).abs() < 1e-3,
|
|
"after seek to {}: first sample = {}, expected {}",
|
|
target,
|
|
buf[0],
|
|
expected
|
|
);
|
|
// And the next few advance in order.
|
|
for k in 0..frames.min(8) {
|
|
let exp = (target as usize + k) as f32 / n as f32;
|
|
assert!((buf[k] - exp).abs() < 1e-3, "seek {}+{}: {} vs {}", target, k, buf[k], exp);
|
|
}
|
|
}
|
|
let _ = std::fs::remove_file(&path);
|
|
}
|
|
|
|
// ── Stage 3: stream video-audio from a byte source (packed .beam blob) ──────────
|
|
|
|
/// A `MediaByteSource` over an in-memory buffer (stands in for a SQLite BlobReader).
|
|
struct VecSource(std::io::Cursor<Vec<u8>>, u64);
|
|
impl std::io::Read for VecSource {
|
|
fn read(&mut self, b: &mut [u8]) -> std::io::Result<usize> { self.0.read(b) }
|
|
}
|
|
impl std::io::Seek for VecSource {
|
|
fn seek(&mut self, p: std::io::SeekFrom) -> std::io::Result<u64> { self.0.seek(p) }
|
|
}
|
|
impl daw_backend::audio::disk_reader::MediaByteSource for VecSource {
|
|
fn byte_len(&self) -> u64 { self.1 }
|
|
}
|
|
|
|
fn ramp_wav_bytes(n: u32, sample_rate: u32) -> Vec<u8> {
|
|
let channels = 1u16;
|
|
let bps = 4u32;
|
|
let data_size = n * bps;
|
|
let mut buf = Vec::with_capacity(44 + data_size as usize);
|
|
buf.extend_from_slice(b"RIFF");
|
|
buf.extend_from_slice(&(36 + data_size).to_le_bytes());
|
|
buf.extend_from_slice(b"WAVE");
|
|
buf.extend_from_slice(b"fmt ");
|
|
buf.extend_from_slice(&16u32.to_le_bytes());
|
|
buf.extend_from_slice(&3u16.to_le_bytes()); // IEEE float
|
|
buf.extend_from_slice(&channels.to_le_bytes());
|
|
buf.extend_from_slice(&sample_rate.to_le_bytes());
|
|
buf.extend_from_slice(&(sample_rate * channels as u32 * bps).to_le_bytes());
|
|
buf.extend_from_slice(&((channels as u32 * bps) as u16).to_le_bytes());
|
|
buf.extend_from_slice(&32u16.to_le_bytes());
|
|
buf.extend_from_slice(b"data");
|
|
buf.extend_from_slice(&data_size.to_le_bytes());
|
|
for i in 0..n {
|
|
buf.extend_from_slice(&((i as f32) / (n as f32)).to_le_bytes());
|
|
}
|
|
buf
|
|
}
|
|
|
|
#[test]
|
|
fn video_audio_open_source_streams_from_bytes() {
|
|
let sr = 8000;
|
|
let n = 4000;
|
|
let bytes = ramp_wav_bytes(n, sr);
|
|
let len = bytes.len() as u64;
|
|
let src = Box::new(VecSource(std::io::Cursor::new(bytes), len));
|
|
|
|
// Open the audio track by streaming from the byte source (no file path).
|
|
let mut reader = VideoAudioReader::open_source(src, Some("wav")).unwrap();
|
|
assert_eq!(reader.channels(), 1);
|
|
assert_eq!(reader.sample_rate(), sr);
|
|
|
|
let mut all: Vec<f32> = Vec::new();
|
|
let mut buf: Vec<f32> = Vec::new();
|
|
loop {
|
|
let frames = reader.decode_next(&mut buf).unwrap();
|
|
if frames == 0 {
|
|
break;
|
|
}
|
|
all.extend_from_slice(&buf);
|
|
}
|
|
assert!(all.len() as u32 >= n - 4, "decoded most of the ramp: {} of {}", all.len(), n);
|
|
// The ramp rises monotonically; sample 0 ≈ 0.0 and the last is near 1.0.
|
|
assert!(all[0].abs() < 1e-3, "first sample ~0, got {}", all[0]);
|
|
assert!(*all.last().unwrap() > 0.9, "last sample ~1.0, got {}", all.last().unwrap());
|
|
}
|