soundcraft/crates/engine/tests/bounce.rs

88 lines
4.2 KiB
Rust

//! Bounces stream the main mix or a bus to their file, exactly as rendered in memory.
use soundcraft_audio_io::{BitDepth, EncodeOptions, FileFormat};
use soundcraft_engine::io::{Bounce, bounce_stems};
use soundcraft_time::Range;
use std::path::PathBuf;
fn scratch(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("soundcraft-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn bounces_write_exactly_the_mix_and_report_its_levels() {
let e = soundcraft_engine::demo::demo_engine();
let s = e.session();
let dir = scratch("bounce-exact");
let path = dir.join("Mix.wav");
let r = Range::new(s.sample_rate.samples(10.0) + 7, s.sample_rate.samples(13.0));
let opts = EncodeOptions { format: FileFormat::Wav, bit_depth: BitDepth::Float32, dither: false, bwf: None };
let verb = s.bus_by_name("Verb").unwrap().id;
for bus in [None, Some(verb)] {
let mut mix = match bus {
Some(b) => soundcraft_mix::render_bus(s, b, r, 1024),
None => soundcraft_mix::render_range(s, r, 1024),
};
assert!(mix.iter().flatten().any(|v| v.abs() > 1e-3), "{bus:?}");
for normalize in [false, true] {
let mut bounce = Bounce::new(e.session_arc(), &path, r, &opts, bus, normalize, false).unwrap();
let mut progress = vec![bounce.progress()];
let done = loop {
if let Some(done) = bounce.step(|| false).unwrap() {
break done;
}
progress.push(bounce.progress());
};
let last = progress[progress.len() - 1];
assert!(progress.len() > 100 && progress.is_sorted() && progress[0] < 0.01 && last > 0.99 && last <= 1.0, "{progress:?}");
if normalize {
soundcraft_dsp::offline::normalize(&mut mix, -0.1, false);
}
let bytes = std::fs::read(&path).unwrap();
let (_, file) = soundcraft_audio_io::decode(&bytes, None).unwrap();
assert_eq!(file.channels, mix, "{bus:?} normalize: {normalize}");
let mut lm = soundcraft_dsp::meter::LoudnessMeter::new(s.sample_rate.as_f64() as f32, mix.len());
lm.process(&mix, mix[0].len());
let levels = (soundcraft_dsp::offline::peak_db(&mix), lm.integrated_lufs(), lm.true_peak_db());
assert_eq!((done.bytes, (done.peak_db, done.lufs, done.true_peak_db)), (bytes.len() as u64, levels));
}
}
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 1);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_bounce_too_big_for_its_format_fails_and_keeps_the_old_file() {
let e = soundcraft_engine::demo::demo_engine();
let s = e.session();
let dir = scratch("bounce-too-big");
let path = dir.join("Mix.aif");
std::fs::write(&path, b"old").unwrap();
let opts = EncodeOptions { format: FileFormat::Aiff, bit_depth: BitDepth::Float32, dither: false, bwf: None };
let err = Bounce::new(e.session_arc(), &path, Range::new(0, s.sample_rate.samples(3.9 * 3600.0)), &opts, None, false, false).err().unwrap();
assert!(err.to_string().contains("4 GiB"), "{err}");
assert_eq!(std::fs::read(&path).unwrap(), b"old");
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 1);
let err = bounce_stems(&e, &dir.to_string_lossy(), Range::new(0, s.sample_rate.samples(4.1 * 3600.0)), &opts).unwrap_err();
assert!(err.to_string().contains("4 hours"), "{err}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn dropping_an_unfinished_bounce_cancels_it_and_keeps_the_old_file() {
let e = soundcraft_engine::demo::demo_engine();
let dir = scratch("bounce-cancel");
let path = dir.join("Mix.wav");
std::fs::write(&path, b"old").unwrap();
let mut bounce = Bounce::new(e.session_arc(), &path, Range::new(0, 48_000), &EncodeOptions::default(), None, false, false).unwrap();
assert_eq!(bounce.step(|| false).unwrap(), None);
assert!(dir.join("Mix.wav.part").exists());
drop(bounce);
assert_eq!(std::fs::read(&path).unwrap(), b"old");
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 1);
let _ = std::fs::remove_dir_all(&dir);
}