use serde_json::{Value, json}; use soundcraft_engine::Engine; use soundcraft_engine::edit::SHUFFLE_LOCK; use soundcraft_model::{AutoParam, Clip, ClipId, EditMode, Source, SourceId, TrackId}; fn source(id: u64, frames: u64) -> Source { Source { id: SourceId(id), name: "src".into(), path: "src.wav".into(), channels: 1, frames, sample_rate: 48_000, format: soundcraft_audio_io::FileFormat::Wav, time_reference: 0, unsaved: false, } } fn new_track(e: &mut Engine) -> TrackId { TrackId(e.execute("track.new", &json!({"count": 1})).unwrap()["tracks"][0].as_u64().unwrap()) } fn engine(mode: &str) -> (Engine, TrackId, [ClipId; 2]) { let mut e = Engine::default(); let t = new_track(&mut e); let s = e.session_mut(); s.sources.push(source(7, 10_000)); let ids = [s.new_clip_id(), s.new_clip_id()]; s.track_mut(t).and_then(|t| t.playlist_mut()).unwrap().clips = vec![Clip::audio(ids[0], "a", SourceId(7), 0, 0, 1000), Clip::audio(ids[1], "b", SourceId(7), 1000, 2000, 1000)]; e.execute("edit.mode", &json!({"mode": mode})).unwrap(); (e, t, ids) } /// A track of clips (start, length, offset) on a long file, source 8. fn long(mode: &str, spans: &[(i64, i64, i64)]) -> (Engine, TrackId, Vec) { let mut e = Engine::default(); let t = new_track(&mut e); let s = e.session_mut(); s.sources.push(source(8, 1_000_000)); let ids: Vec = spans.iter().map(|_| s.new_clip_id()).collect(); s.track_mut(t).and_then(|t| t.playlist_mut()).unwrap().clips = spans.iter().zip(&ids).map(|(&(start, len, offset), id)| Clip::audio(*id, "c", SourceId(8), offset, start, len)).collect(); e.execute("edit.mode", &json!({"mode": mode})).unwrap(); (e, t, ids) } fn clips(e: &Engine, t: TrackId) -> Vec<(i64, i64)> { e.session().track(t).unwrap().clips().iter().map(|c| (c.start, c.length)).collect() } /// Each clip's id, length and file offset, in id order: what a move must never change. fn material(e: &Engine, t: TrackId) -> Vec<(u64, i64, i64)> { let mut v: Vec<_> = e.session().track(t).unwrap().clips().iter().map(|c| (c.id.0, c.length, c.source_offset())).collect(); v.sort_unstable(); v } fn start(e: &Engine, id: ClipId) -> i64 { e.session().find_clip(id).unwrap().1.start } fn points(e: &Engine, t: TrackId) -> Vec { e.session().track(t).unwrap().lane(&AutoParam::Volume).map(|l| l.points.iter().map(|p| p.at).collect()).unwrap_or_default() } fn set_points(e: &mut Engine, t: TrackId, at: &[i64]) { for a in at { run(e, "automation.set_point", json!({"track": t.0, "param": "volume", "at": a, "value": -6.0})); } } fn run(e: &mut Engine, id: &str, p: Value) { e.execute(id, &p).unwrap(); } #[test] fn insert_silence_pushes_in_shuffle_and_clears_otherwise() { for (mode, want) in [("shuffle", vec![(0, 500), (700, 500), (2200, 1000)]), ("slip", vec![(0, 500), (700, 300), (2000, 1000)])] { let (mut e, t, _) = engine(mode); run(&mut e, "edit.insert_silence", json!({"tracks": [t.0], "start": 500, "end": 700})); assert_eq!(clips(&e, t), want, "{mode}"); } } #[test] fn duplicate_and_repeat_make_room_in_shuffle() { let (mut e, t, _) = engine("shuffle"); run(&mut e, "edit.duplicate", json!({"tracks": [t.0], "start": 0, "end": 1000})); assert_eq!(clips(&e, t), vec![(0, 1000), (1000, 1000), (3000, 1000)]); let (mut e, t, _) = engine("shuffle"); run(&mut e, "edit.repeat", json!({"tracks": [t.0], "start": 0, "end": 1000, "count": 2})); assert_eq!(clips(&e, t), vec![(0, 1000), (1000, 1000), (2000, 1000), (4000, 1000)]); let (mut e, t, _) = engine("slip"); run(&mut e, "edit.duplicate", json!({"tracks": [t.0], "start": 0, "end": 1000})); assert_eq!(clips(&e, t), vec![(0, 1000), (1000, 1000), (2000, 1000)]); } #[test] fn shuffle_paste_replaces_the_selection() { let (mut e, t, _) = engine("shuffle"); run(&mut e, "edit.copy", json!({"tracks": [t.0], "start": 0, "end": 1000})); run(&mut e, "edit.select", json!({"tracks": [t.0], "start": 2000, "end": 2500})); run(&mut e, "edit.paste", json!({})); assert_eq!(clips(&e, t), vec![(0, 1000), (2000, 1000), (3000, 500)]); } #[test] fn clip_list_drops_land_on_a_clip_boundary_in_shuffle() { let (mut e, t, [_, b]) = engine("shuffle"); run(&mut e, "clip.place_source", json!({"source": 7, "track": t.0, "at": 1900})); assert_eq!(clips(&e, t), vec![(0, 1000), (2000, 10_000), (12_000, 1000)]); e.session_mut().find_clip_mut(b).unwrap().time_locked = true; assert!(e.execute("clip.place_source", &json!({"source": 7, "track": t.0, "at": 0})).is_err()); assert_eq!(clips(&e, t).len(), 3); } #[test] fn grid_moves_to_a_position_snap_or_keep_their_offset() { let (mut e, t, [_, b]) = engine("grid"); let s = e.session(); let step = s.edit.grid.step_samples(0, s.sample_rate, &s.tempo, s.frame_rate); run(&mut e, "edit.move_clips", json!({"clips": [b.0], "to": step + 1000})); assert_eq!(clips(&e, t)[1].0, step); run(&mut e, "edit.mode", json!({"mode": "relative_grid"})); run(&mut e, "edit.move_clips", json!({"clips": [b.0], "to": 2 * step + 2000})); assert_eq!(clips(&e, t)[1].0, 2 * step); run(&mut e, "edit.move_clips", json!({"clips": [b.0], "by": 200})); run(&mut e, "edit.move_clips", json!({"clips": [b.0], "to": 3 * step})); assert_eq!(clips(&e, t)[1].0, 3 * step + 200); } #[test] fn clips_cannot_move_onto_tracks_without_a_playlist() { let (mut e, t, [a, _]) = engine("slip"); let aux = e.execute("track.new", &json!({"kind": "aux"})).unwrap()["tracks"][0].clone(); assert!(e.execute("edit.move_clips", &json!({"clips": [a.0], "by": 0, "track": aux})).is_err()); assert_eq!(clips(&e, t).len(), 2); } #[test] fn trim_clip_needs_an_edge_and_an_unlocked_clip() { let (mut e, t, [a, b]) = engine("slip"); assert!(e.execute("edit.trim_clip", &json!({"clip": a.0})).is_err()); e.session_mut().find_clip_mut(b).unwrap().edit_locked = true; assert!(e.execute("edit.trim_clip", &json!({"clip": b.0, "end": 2500})).is_err()); run(&mut e, "edit.trim_clip", json!({"clip": a.0, "end": 5000})); assert_eq!(clips(&e, t), vec![(0, 2000), (2000, 1000)]); } #[test] fn shuffle_lock_keeps_shuffle_mode_out() { let (mut e, _, _) = engine("slip"); run(&mut e, "edit.shuffle_lock", json!({})); assert!(e.execute("edit.mode", &json!({"mode": "shuffle"})).is_err()); run(&mut e, "edit.mode", json!({"mode": "grid"})); run(&mut e, "edit.shuffle_lock", json!({"value": false})); run(&mut e, "edit.mode", json!({"mode": "shuffle"})); assert!(e.execute("edit.shuffle_lock", &json!({})).is_err()); } #[test] fn snap_to_grid_rides_along_with_the_other_modes() { let (mut e, _, _) = engine("slip"); let snap = |e: &Engine| e.session().edit.flag(soundcraft_engine::edit::SNAP_TO_GRID); run(&mut e, "edit.snap_to_grid", json!({})); assert!(snap(&e)); run(&mut e, "edit.mode", json!({"mode": "spot", "snap_to_grid": true})); assert!(snap(&e)); run(&mut e, "edit.mode", json!({"mode": "shuffle"})); assert!(!snap(&e)); run(&mut e, "edit.mode", json!({"mode": "grid", "snap_to_grid": true})); assert!(!snap(&e)); assert!(e.execute("edit.snap_to_grid", &json!({})).is_err()); } #[test] fn shuffle_edits_that_would_push_a_locked_clip_are_refused() { let (mut e, t, [_, b]) = engine("shuffle"); e.session_mut().find_clip_mut(b).unwrap().time_locked = true; for id in ["edit.insert_silence", "edit.duplicate"] { let err = e.execute(id, &json!({"tracks": [t.0], "start": 0, "end": 1500})).unwrap_err().to_string(); assert!(err.contains("locked clip"), "{id}: {err}"); } run(&mut e, "edit.insert_silence", json!({"tracks": [t.0], "start": 0, "end": 1000})); assert_eq!(clips(&e, t), vec![(1000, 1000), (2000, 1000)]); run(&mut e, "event.cut_time", json!({"start": 0, "end": 1000})); assert_eq!(clips(&e, t), vec![(0, 1000), (1000, 1000)]); } #[test] fn clips_dragged_from_several_tracks_keep_their_tracks_apart() { let (mut e, t, [a, _]) = engine("slip"); let new = e.execute("track.new", &json!({"count": 2})).unwrap()["tracks"].clone(); let [t2, t3] = [0, 1].map(|i| TrackId(new[i].as_u64().unwrap())); let s = e.session_mut(); let d = s.new_clip_id(); s.track_mut(t2).and_then(|t| t.playlist_mut()).unwrap().clips.push(Clip::audio(d, "d", SourceId(7), 0, 5000, 500)); run(&mut e, "edit.move_clips", json!({"clips": [a.0, d.0], "by": 0, "track": t2.0})); assert_eq!(clips(&e, t), vec![(2000, 1000)]); assert_eq!(clips(&e, t2), vec![(0, 1000)]); assert_eq!(clips(&e, t3), vec![(5000, 500)]); assert!(e.execute("edit.move_clips", &json!({"clips": [a.0, d.0], "by": 0, "track": t3.0})).is_err()); } #[test] fn relative_grid_pastes_keep_the_offset_from_the_grid() { let (mut e, t, [_, b]) = engine("relative_grid"); let s = e.session(); let step = s.edit.grid.step_samples(0, s.sample_rate, &s.tempo, s.frame_rate); run(&mut e, "edit.select", json!({"clips": [b.0]})); run(&mut e, "edit.copy", json!({})); run(&mut e, "edit.select", json!({"tracks": [t.0], "start": 3 * step})); run(&mut e, "edit.paste", json!({})); assert_eq!(clips(&e, t).last(), Some(&(3 * step + 2000, 1000))); run(&mut e, "edit.mode", json!({"mode": "slip"})); run(&mut e, "edit.select", json!({"tracks": [t.0], "start": 5 * step})); run(&mut e, "edit.paste", json!({})); assert_eq!(clips(&e, t).last(), Some(&(5 * step, 1000))); } #[test] fn shuffle_moves_keep_crossfade_partners_whole() { let (mut e, t, ids) = long("slip", &[(0, 1000, 10_000), (1000, 1000, 20_000), (2000, 1000, 30_000)]); run(&mut e, "edit.fades_create", json!({"tracks": [t.0], "start": 900, "end": 1100})); run(&mut e, "edit.mode", json!({"mode": "shuffle"})); let whole = material(&e, t); let [a, b, c] = [ids[0], ids[1], ids[2]]; run(&mut e, "edit.move_clips", json!({"clips": [a.0], "by": 5000})); assert_eq!(material(&e, t), whole); assert_eq!([start(&e, b), start(&e, c), start(&e, a)], [0, 1100, 2100]); let (mut e, t, ids) = long("slip", &[(0, 1000, 10_000), (1000, 1000, 20_000), (2000, 1000, 30_000)]); run(&mut e, "edit.fades_create", json!({"tracks": [t.0], "start": 900, "end": 1100})); run(&mut e, "edit.mode", json!({"mode": "shuffle"})); for by in [2500, -2500] { run(&mut e, "edit.move_clips", json!({"clips": [ids[0].0, ids[1].0], "by": by})); assert_eq!(material(&e, t), whole, "{by}"); assert_eq!(start(&e, ids[1]) - start(&e, ids[0]), 900, "{by}"); } assert_eq!([start(&e, ids[0]), start(&e, ids[2])], [0, 2000]); run(&mut e, "edit.mode", json!({"mode": "slip"})); run(&mut e, "edit.move_clips", json!({"clips": [ids[0].0, ids[1].0], "by": 5000})); assert_eq!(material(&e, t), whole); } #[test] fn pasting_copied_automation_in_shuffle_leaves_the_clips_alone() { let (mut e, t, ids) = long("slip", &[(0, 1000, 0), (1000, 1000, 0), (2000, 1000, 0)]); set_points(&mut e, t, &[200]); run(&mut e, "edit.copy_all_automation", json!({"tracks": [t.0], "start": 0, "end": 500})); run(&mut e, "edit.mode", json!({"mode": "shuffle"})); run(&mut e, "edit.select", json!({"clips": [ids[1].0]})); run(&mut e, "edit.paste", json!({})); assert_eq!(clips(&e, t), vec![(0, 1000), (1000, 1000), (2000, 1000)]); } #[test] fn automation_follows_shuffle_moves_trims_and_drops() { let setup = || { let (mut e, t, ids) = long("shuffle", &[(0, 1000, 0), (1000, 1000, 0), (2000, 1000, 0)]); set_points(&mut e, t, &[500, 1500, 2500]); (e, t, ids) }; let (mut e, t, ids) = setup(); run(&mut e, "edit.move_clips", json!({"clips": [ids[0].0], "by": 3000})); assert_eq!(points(&e, t), [500, 1500, 2500]); assert_eq!([start(&e, ids[1]), start(&e, ids[2]), start(&e, ids[0])], [0, 1000, 2000]); let (mut e, t, ids) = setup(); run(&mut e, "edit.trim_clip", json!({"clip": ids[0].0, "end": 500})); assert_eq!(points(&e, t), [1000, 2000]); let (mut e, t, _) = setup(); run(&mut e, "clip.place_source", json!({"source": 8, "track": t.0, "at": 1000})); assert_eq!(points(&e, t), [500, 1_001_500, 1_002_500]); } #[test] fn shuffle_automation_stops_at_locked_clips_and_needs_automation_follows_edit() { let (mut e, t, ids) = long("shuffle", &[(0, 1000, 0), (1000, 1000, 0), (3000, 1000, 0)]); e.session_mut().find_clip_mut(ids[2]).unwrap().time_locked = true; set_points(&mut e, t, &[3500]); run(&mut e, "edit.insert_silence", json!({"tracks": [t.0], "start": 0, "end": 500})); run(&mut e, "edit.cut", json!({"tracks": [t.0], "start": 0, "end": 500})); assert_eq!((start(&e, ids[2]), points(&e, t)), (3000, vec![3500])); let (mut e, t, _) = long("shuffle", &[(0, 1000, 0), (1000, 1000, 0)]); set_points(&mut e, t, &[300, 1500, 5000]); e.session_mut().edit.automation_follows_edit = false; run(&mut e, "edit.duplicate", json!({"tracks": [t.0], "start": 0, "end": 1000})); run(&mut e, "edit.insert_silence", json!({"tracks": [t.0], "start": 0, "end": 100})); assert_eq!(points(&e, t), [300, 1500, 5000]); } #[test] fn shuffle_clear_moves_automation_with_the_clips() { let (mut e, t, _) = long("shuffle", &[(0, 10_000, 0), (20_000, 10_000, 0)]); set_points(&mut e, t, &[0, 9_000, 20_000, 29_000]); run(&mut e, "edit.clear", json!({"track": t.0, "start": 4_000, "end": 6_000})); assert_eq!(clips(&e, t), [(0, 4_000), (4_000, 4_000), (18_000, 10_000)]); assert_eq!(points(&e, t), [0, 7_000, 18_000, 27_000]); } #[test] fn relative_grid_pastes_land_on_grid_lines_at_any_tempo() { let (mut e, t, [a, _]) = engine("relative_grid"); run(&mut e, "event.tempo", json!({"bpm": 90, "at": 0})); run(&mut e, "edit.select", json!({"clips": [a.0]})); run(&mut e, "edit.copy", json!({})); run(&mut e, "edit.select", json!({"tracks": [t.0], "start": 128_000})); run(&mut e, "edit.paste", json!({})); assert!(clips(&e, t).contains(&(128_000, 1000)), "{:?}", clips(&e, t)); run(&mut e, "edit.paste", json!({"tracks": [t.0], "at": 50_000})); assert!(clips(&e, t).contains(&(50_000, 1000)), "{:?}", clips(&e, t)); } #[test] fn start_trims_keep_the_sync_point_on_its_audio() { let (mut e, t, ids) = long("slip", &[(0, 10_000, 0)]); let sync = |e: &Engine| e.session().find_clip(ids[0]).unwrap().1.sync_point; e.session_mut().find_clip_mut(ids[0]).unwrap().sync_point = 5000; run(&mut e, "edit.trim_clip", json!({"clip": ids[0].0, "start": 2000})); assert_eq!(sync(&e), 3000); run(&mut e, "edit.trim_clip", json!({"clip": ids[0].0, "start": 8000})); assert_eq!(sync(&e), 0); run(&mut e, "edit.mode", json!({"mode": "grid"})); run(&mut e, "edit.move_clips", json!({"clips": [ids[0].0], "to": 30_000})); assert_eq!(clips(&e, t), vec![(30_000, 2000)]); } #[test] fn imported_time_stamps_count_at_the_session_rate() { use soundcraft_audio_io::{AudioBuffer, BwfInfo, EncodeOptions, FileFormat}; let mut e = Engine::default(); let bwf = BwfInfo { time_reference: 1_587_600_000, ..BwfInfo::default() }; let opts = EncodeOptions { format: FileFormat::Wav, bwf: Some(bwf), ..EncodeOptions::default() }; let bytes = soundcraft_audio_io::encode(&AudioBuffer::new(44_100, 1, 441), &opts).unwrap(); soundcraft_engine::io::import_audio_bytes(&mut e, "stamped.wav", &bytes, None, None, 0).unwrap(); assert_eq!(e.session().sources[0].time_reference, 1_728_000_000); } #[test] fn shuffle_trims_stop_at_a_locked_crossfade_and_stay_inside_the_file() { let (mut e, t, ids) = long("slip", &[(0, 1000, 500), (1000, 1000, 5000)]); run(&mut e, "edit.fades_create", json!({"tracks": [t.0], "start": 950, "end": 1050})); e.session_mut().find_clip_mut(ids[1]).unwrap().time_locked = true; run(&mut e, "edit.mode", json!({"mode": "shuffle"})); let before = material(&e, t); run(&mut e, "edit.trim_clip", json!({"clip": ids[0].0, "end": 1500})); run(&mut e, "edit.trim_clip", json!({"clip": ids[0].0, "start": 0})); assert_eq!(material(&e, t), before); let (mut e, _, [_, b]) = engine("shuffle"); run(&mut e, "edit.trim_clip", json!({"clip": b.0, "start": 2500, "end": 20_000})); let c = e.session().find_clip(b).unwrap().1; assert_eq!((c.source_offset(), c.source_offset() + c.length), (1500, 10_000)); } #[test] fn duplicate_and_extend_makes_room_in_shuffle() { let (mut e, t, _) = long("shuffle", &[(0, 1000, 0), (1000, 1000, 0)]); run(&mut e, "edit.duplicate_extend", json!({"tracks": [t.0], "start": 0, "end": 1000})); assert_eq!(clips(&e, t), vec![(0, 1000), (1000, 1000), (2000, 1000)]); let (mut e, t, ids) = long("shuffle", &[(0, 1000, 0), (1000, 1000, 0)]); e.session_mut().find_clip_mut(ids[1]).unwrap().time_locked = true; assert!(e.execute("edit.duplicate_extend", &json!({"tracks": [t.0], "start": 0, "end": 1000})).is_err()); assert_eq!(clips(&e, t), vec![(0, 1000), (1000, 1000)]); } #[test] fn undo_leaves_the_edit_mode_and_shuffle_lock_alone() { let (mut e, t, _) = engine("shuffle"); run(&mut e, "edit.cut", json!({"tracks": [t.0], "start": 0, "end": 500})); run(&mut e, "edit.mode", json!({"mode": "slip"})); run(&mut e, "edit.shuffle_lock", json!({})); for step in [Engine::undo, Engine::redo] { assert!(step(&mut e)); assert_eq!((e.session().edit.edit_mode, e.session().edit.flag(SHUFFLE_LOCK)), (EditMode::Slip, true)); } } #[test] fn shuffle_cuts_reaching_before_the_session_start_keep_the_spacing() { let (mut e, t, _) = long("shuffle", &[(0, 1000, 0), (1000, 1000, 0), (2000, 1000, 0)]); run(&mut e, "edit.cut", json!({"tracks": [t.0], "start": -5000, "end": 500})); assert_eq!(clips(&e, t), vec![(0, 500), (500, 1000), (1500, 1000)]); } #[test] fn shuffle_never_pulls_a_crossfade_apart() { let (mut e, t, ids) = long("slip", &[(0, 9000, 0), (9000, 11_000, 100_000), (20_000, 5000, 200_000)]); run(&mut e, "edit.fades_create", json!({"tracks": [t.0], "start": 8000, "end": 10_000})); run(&mut e, "edit.mode", json!({"mode": "shuffle"})); let err = e.execute("edit.duplicate", &json!({"tracks": [t.0], "start": 5000, "end": 9000})).unwrap_err().to_string(); assert!(err.contains("crossfade"), "{err}"); run(&mut e, "clip.place_source", json!({"source": 8, "track": t.0, "at": 7900})); assert_eq!(clips(&e, t)[0], (0, 1_000_000)); assert_eq!([start(&e, ids[0]), start(&e, ids[1])], [1_000_000, 1_008_000]); } #[test] fn clips_move_to_and_from_hidden_tracks() { let (mut e, t, [a, _]) = engine("slip"); let hidden = new_track(&mut e); run(&mut e, "track.hide", json!({"tracks": [hidden.0], "hidden": true})); run(&mut e, "edit.move_clips", json!({"clips": [a.0], "by": 0, "track": hidden.0})); assert_eq!(clips(&e, hidden), vec![(0, 1000)]); run(&mut e, "edit.move_clips", json!({"clips": [a.0], "by": 0, "track": t.0})); assert_eq!(clips(&e, t).len(), 2); } #[test] fn shuffle_lock_and_snap_rules_hold_when_a_call_names_tracks() { let (mut e, t, _) = engine("shuffle"); assert!(e.execute("edit.shuffle_lock", &json!({"value": true, "tracks": [t.0]})).is_err()); run(&mut e, "edit.mode", json!({"mode": "grid"})); assert!(e.execute("edit.snap_to_grid", &json!({"tracks": [t.0]})).is_err()); }