soundcraft/crates/engine/tests/consolidate_selection.rs

125 lines
4.9 KiB
Rust

//! Consolidate makes one clip per track that covers exactly the edit selection (#164).
use serde_json::json;
use soundcraft_audio_io::{AudioBuffer, FileFormat};
use soundcraft_engine::{Engine, EngineError};
use soundcraft_model::{Clip, TrackId};
use soundcraft_time::Range;
fn stereo_tracks(n: u64) -> Engine {
let mut e = Engine::default();
e.execute("track.new", &json!({"count": n, "format": "stereo"})).unwrap();
e.session_mut().edit.selected_tracks = e.session().tracks.iter().map(|t| t.id).collect();
e
}
fn sr(e: &Engine) -> i64 {
e.session().sample_rate.samples(1.0)
}
/// Put a constant-level clip of `secs` seconds at the start of track `i`.
fn add_clip(e: &mut Engine, i: usize, secs: i64) -> TrackId {
let frames = usize::try_from(secs * sr(e)).unwrap();
let t = e.session().tracks[i].id;
let buf = AudioBuffer { sample_rate: 48_000, channels: vec![vec![0.25; frames]; 2] };
let src = soundcraft_engine::io::add_source(e.session_mut(), "tone", buf, None, FileFormat::Wav);
let id = e.session_mut().new_clip_id();
soundcraft_engine::edit::place_clip(e.session_mut(), t, Clip::audio(id, "tone", src, 0, 0, frames as i64));
t
}
fn clips(e: &Engine, i: usize) -> Vec<(i64, i64)> {
e.session().tracks[i].clips().iter().map(|c| (c.start, c.end())).collect()
}
/// Frames in the source behind the only clip on track `i`, and its first sample.
fn source_of_only_clip(e: &Engine, i: usize) -> (usize, f32) {
let s = e.session();
let c = &s.tracks[i].clips()[0];
let audio = s.pool.get(c.source().unwrap()).unwrap();
(audio.buffer.frames(), audio.buffer.channels[0][0])
}
fn assert_each_track_has(e: &Engine, start: i64, end: i64) {
for (i, t) in e.session().tracks.iter().enumerate() {
assert_eq!(clips(e, i), vec![(start, end)], "track {}", t.name);
assert_eq!(source_of_only_clip(e, i).0 as i64, end - start, "track {}", t.name);
}
}
#[test]
fn consolidating_silence_from_zero_covers_the_whole_selection() {
let mut e = stereo_tracks(5);
let sr = sr(&e);
e.session_mut().edit.selection = Range::new(0, 5 * sr);
e.execute("edit.consolidate", &json!({})).unwrap();
assert_each_track_has(&e, 0, 5 * sr);
}
#[test]
fn consolidating_silence_later_in_the_timeline_covers_the_whole_selection() {
let mut e = stereo_tracks(5);
let sr = sr(&e);
e.session_mut().edit.selection = Range::new(2 * sr, 7 * sr);
e.execute("edit.consolidate", &json!({})).unwrap();
assert_each_track_has(&e, 2 * sr, 7 * sr);
}
#[test]
fn consolidating_past_the_end_of_a_clip_keeps_the_trailing_silence() {
let mut e = stereo_tracks(1);
let sr = sr(&e);
add_clip(&mut e, 0, 1);
e.session_mut().edit.selection = Range::new(0, 5 * sr);
e.execute("edit.consolidate", &json!({})).unwrap();
assert_each_track_has(&e, 0, 5 * sr);
assert_eq!(source_of_only_clip(&e, 0).1, 0.25);
}
#[test]
fn multi_track_consolidate_is_one_undo_step_and_fills_empty_tracks() {
let mut e = stereo_tracks(3);
let sr = sr(&e);
add_clip(&mut e, 1, 2);
let before = e.session().clone();
let undo_depth = e.undo_history().len();
e.session_mut().edit.selection = Range::new(sr, 4 * sr);
e.execute("edit.consolidate", &json!({})).unwrap();
// The clip on track 2 is split at the selection start; the head stays put.
assert_eq!(clips(&e, 0), vec![(sr, 4 * sr)]);
assert_eq!(clips(&e, 1), vec![(0, sr), (sr, 4 * sr)]);
assert_eq!(clips(&e, 2), vec![(sr, 4 * sr)]);
assert_eq!(e.undo_history().len(), undo_depth + 1);
assert!(e.undo());
for (a, b) in e.session().tracks.iter().zip(before.tracks.iter()) {
assert_eq!(a.clips(), b.clips(), "track {}", a.name);
}
}
#[test]
fn a_selection_too_long_to_render_is_refused_not_shortened() {
let mut e = stereo_tracks(2);
add_clip(&mut e, 0, 1);
let before: Vec<_> = (0..2).map(|i| clips(&e, i)).collect();
// A stereo track renders at most MAX_RENDER_SAMPLES / 2 frames (about 93 minutes at 48 kHz).
let max = i64::try_from(soundcraft_mix::MAX_RENDER_SAMPLES / 2).unwrap();
let err = e.execute("edit.consolidate", &json!({"start": 0, "end": max + 1})).unwrap_err();
assert!(matches!(err, EngineError::BadParams(..)), "{err}");
assert!(err.to_string().contains("too long"), "{err}");
let after: Vec<_> = (0..2).map(|i| clips(&e, i)).collect();
assert_eq!(after, before);
}
#[test]
fn prepare_dpe_still_consolidates_each_track_to_its_own_material() {
let mut e = stereo_tracks(2);
let sr = sr(&e);
let a = add_clip(&mut e, 0, 1);
let b = add_clip(&mut e, 1, 3);
e.execute("options.prepare_dpe_tracks", &json!({"tracks": [a.0, b.0]})).unwrap();
// Unchanged from before #164: each track runs to its last clip plus a second, capped at the session end.
assert_eq!(clips(&e, 0), vec![(0, 2 * sr)]);
assert_eq!(clips(&e, 1), vec![(0, 3 * sr)]);
assert_eq!(source_of_only_clip(&e, 0), (2 * sr as usize, 0.25));
}