125 lines
4.9 KiB
Rust
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));
|
|
}
|