Examples
session_undo
examples/primitives/session_undo.rs, the whole program
Run it with cargo run --example session_undo (see the examples for the features some need).
//! An undo stack builds a memory, then three undos and three redos walk it.
//! Saving writes 2927 bytes that reload identical; one more undo is dirty.
//!
//! ```text
//! cargo run --example session_undo
//! ```
use superb::primitives::session::commands::{
self, AddClip, AddMarker, AddSend, AddTrack, MoveClip, SetTempo, SetTrackOutput,
};
use superb::primitives::session::{
Clip, ClipSource, Marker, Memory, MidiNote, Track, TrackKind, TrackSend,
};
use superb::primitives::undo::UndoStack;
use superb::primitives::{BeatPosition, Result, SampleRate, TimeDuration, TimePosition};
// One line: name, revision, counts, tempo, end, and the undo/redo labels.
fn describe(memory: &Memory, history: &UndoStack<Memory>) {
println!(
" {:<12} rev {:>3} tracks {} clips {} sends {} markers {} tempo {:.0} bpm end {} smp | undo: {:?} redo: {:?}",
memory.name(),
memory.revision(),
memory.track_count(),
memory.clip_count(),
memory.send_count(),
memory.markers().len(),
memory.tempo_map().tempo_at(BeatPosition::ZERO),
memory.end_position().samples(),
history.undo_label().unwrap_or("-"),
history.redo_label().unwrap_or("-"),
);
}
fn main() -> Result<()> {
let rate = SampleRate::HZ_48K;
assert_eq!(rate.seconds_to_samples(1.0), 48_000);
let mut memory = Memory::new("untitled", rate);
let mut history = UndoStack::new();
println!("edits:");
// The memory is named "demo memory" and the tempo at beat 0 is 96 bpm.
history.push(&mut memory, commands::set_session_name("demo memory"))?;
history.push(&mut memory, SetTempo::new(BeatPosition::ZERO, 96.0))?;
assert_eq!(memory.name(), "demo memory");
assert!((memory.tempo_map().tempo_at(BeatPosition::ZERO) - 96.0).abs() < 1e-9);
// One step, "Add drum track": an audio track coloured 0xE06030 and a
// 2-second clip (96000 samples). Five applies, so revision 5.
history.begin_group("Add drum track");
let drums = memory.allocate_track_id();
history.push(
&mut memory,
AddTrack::new(Track::with_id(drums, "drums", TrackKind::Audio)),
)?;
history.push(
&mut memory,
commands::set_track_color(drums, Some(0xE0_60_30)),
)?;
let loop_id = memory.allocate_clip_id();
let loop_clip = Clip::with_id(
loop_id,
"break.wav",
TimePosition::ZERO,
TimeDuration::from_seconds(2.0, rate),
ClipSource::Audio {
path: "loops/break.wav".into(),
offset: TimeDuration::ZERO,
},
)?;
history.push(&mut memory, AddClip::new(drums, loop_clip))?;
history.end_group();
assert_eq!(memory.revision(), 5);
assert_eq!(memory.track_count(), 1);
assert_eq!(memory.clip_count(), 1);
assert_eq!(memory.send_count(), 0);
assert!(memory.markers().is_empty());
assert_eq!(memory.track(drums)?.name, "drums");
assert_eq!(memory.track(drums)?.kind, TrackKind::Audio);
assert_eq!(memory.track(drums)?.color, Some(0xE0_60_30));
assert_eq!(memory.clip(loop_id)?.length().samples(), 96_000);
assert_eq!(memory.end_position().samples(), 96_000);
assert_eq!(history.undo_label(), Some("Add drum track"));
assert!(history.redo_label().is_none());
describe(&memory, &history);
// A MIDI track and a 4-beat clip placed at beat 4. At 96 bpm that is
// sample 120000, length 120000, so the memory ends at 240000.
// Notes 60, 64 and 67, velocity 96, channel 0, each 2 beats. Revision 7.
let keys = memory.allocate_track_id();
history.push(
&mut memory,
AddTrack::new(Track::with_id(keys, "keys", TrackKind::Midi)),
)?;
let chord = memory.allocate_clip_id();
let notes = vec![
MidiNote {
beat: 0.0,
length: 2.0,
note: 60,
velocity: 96,
channel: 0,
},
MidiNote {
beat: 0.0,
length: 2.0,
note: 64,
velocity: 96,
channel: 0,
},
MidiNote {
beat: 0.0,
length: 2.0,
note: 67,
velocity: 96,
channel: 0,
},
];
let bar = memory.tempo_map().beat_to_samples(BeatPosition::new(4.0));
assert_eq!(bar.samples(), 120_000);
let chord_clip = Clip::with_id(
chord,
"C major",
bar,
TimeDuration::from_samples(bar.samples()),
ClipSource::Midi { notes },
)?;
history.push(&mut memory, AddClip::new(keys, chord_clip))?;
assert_eq!(memory.revision(), 7);
assert_eq!(memory.track_count(), 2);
assert_eq!(memory.clip_count(), 2);
assert_eq!(memory.track(keys)?.kind, TrackKind::Midi);
assert_eq!(memory.clip(chord)?.start().samples(), 120_000);
assert_eq!(memory.clip(chord)?.length().samples(), 120_000);
assert_eq!(memory.end_position().samples(), 240_000);
let ClipSource::Midi { notes } = &memory.clip(chord)?.source else {
panic!("chord clip is midi");
};
assert_eq!(notes.len(), 3);
for (note, number) in notes.iter().zip([60_u8, 64, 67]) {
assert_eq!(note.note, number);
assert_eq!(note.velocity, 96);
assert_eq!(note.channel, 0);
assert!(note.beat.abs() < 1e-9);
assert!((note.length - 2.0).abs() < 1e-9);
}
assert_eq!(history.undo_label(), Some("Add clip"));
describe(&memory, &history);
// Keys route to a bus. Drums send to it pre-fader. Levels −3, −6 and
// −9 coalesce into one "Set send level", left at −9 dB. Revision 13.
// The end is still 240000.
let verb = memory.allocate_track_id();
history.push(
&mut memory,
AddTrack::new(Track::with_id(verb, "verb", TrackKind::Bus)),
)?;
history.push(&mut memory, SetTrackOutput::new(keys, Some(verb)))?;
let send = memory.allocate_send_id();
history.push(
&mut memory,
AddSend::new(drums, TrackSend::with_id(send, verb).pre_fader()),
)?;
for db in [-3.0, -6.0, -9.0] {
history.push(&mut memory, commands::set_send_level_db(send, db))?;
}
history.seal();
assert_eq!(memory.revision(), 13);
assert_eq!(memory.track_count(), 3);
assert_eq!(memory.send_count(), 1);
assert_eq!(memory.track(verb)?.kind, TrackKind::Bus);
assert_eq!(memory.track(keys)?.output(), Some(verb));
assert!(memory.send(send)?.pre_fader);
assert_eq!(memory.send(send)?.bus(), verb);
assert!((memory.send(send)?.state.level_db + 9.0).abs() < 1e-6);
assert_eq!(memory.end_position().samples(), 240_000);
assert_eq!(history.undo_label(), Some("Set send level"));
describe(&memory, &history);
// Moves to beats 5, 6, 7 and 8 coalesce into one "Move clip". Beat 8
// is sample 240000; the clip still lasts 120000, so the end is 360000.
// A marker named "verse" sits at sample 120000. Revision 18.
for beat in [5.0, 6.0, 7.0, 8.0] {
let pos = memory.tempo_map().beat_to_samples(BeatPosition::new(beat));
history.push(&mut memory, MoveClip::new(chord, pos))?;
}
history.seal();
assert_eq!(
memory
.tempo_map()
.beat_to_samples(BeatPosition::new(8.0))
.samples(),
240_000
);
let marker = memory.allocate_marker_id();
history.push(
&mut memory,
AddMarker::new(Marker::with_id(marker, "verse", bar)),
)?;
assert_eq!(memory.revision(), 18);
assert_eq!(memory.markers().len(), 1);
assert_eq!(memory.markers()[0].name, "verse");
assert_eq!(memory.markers()[0].position().samples(), 120_000);
assert_eq!(memory.clip(chord)?.start().samples(), 240_000);
assert_eq!(memory.end_position().samples(), 360_000);
assert_eq!(history.undo_label(), Some("Add marker"));
describe(&memory, &history);
// Eleven steps. Most recent first. The level drag and the move drag
// are one step each, so eighteen applies became eleven steps.
let labels: Vec<_> = history.undo_labels().collect();
assert_eq!(history.undo_len(), 11);
assert_eq!(
labels,
[
"Add marker",
"Move clip",
"Set send level",
"Add send",
"Route track",
"Add track",
"Add clip",
"Add track",
"Add drum track",
"Set tempo",
"Rename session",
]
);
println!(
" history ({} steps): {:?}",
history.undo_len(),
history.undo_labels().collect::<Vec<_>>()
);
// Undo drops the marker (revision 19, end still 360000), then the move
// (clip back at 120000, end 240000, revision 20), then the send level
// (back to 0 dB, revision 21).
println!("undo x3:");
for step in 0..3 {
history.undo(&mut memory)?;
describe(&memory, &history);
match step {
0 => {
assert!(memory.markers().is_empty());
assert_eq!(memory.end_position().samples(), 360_000);
assert_eq!(memory.revision(), 19);
assert_eq!(history.undo_label(), Some("Move clip"));
assert_eq!(history.redo_label(), Some("Add marker"));
}
1 => {
assert_eq!(memory.clip(chord)?.start().samples(), 120_000);
assert_eq!(memory.end_position().samples(), 240_000);
assert_eq!(memory.revision(), 20);
assert!((memory.send(send)?.state.level_db + 9.0).abs() < 1e-6);
assert_eq!(history.undo_label(), Some("Set send level"));
assert_eq!(history.redo_label(), Some("Move clip"));
}
_ => {
assert!(memory.send(send)?.state.level_db.abs() < 1e-6);
assert_eq!(memory.end_position().samples(), 240_000);
assert_eq!(memory.revision(), 21);
assert_eq!(history.undo_label(), Some("Add send"));
assert_eq!(history.redo_label(), Some("Set send level"));
}
}
}
// Redo restores −9 dB, the clip end at 360000, and the marker.
// Revision 24, and the redo stack is empty.
println!("redo x3:");
let mut redos = 0;
while history.redo(&mut memory)? {
describe(&memory, &history);
redos += 1;
}
assert_eq!(redos, 3);
assert_eq!(memory.revision(), 24);
assert!((memory.send(send)?.state.level_db + 9.0).abs() < 1e-6);
assert_eq!(memory.clip(chord)?.start().samples(), 240_000);
assert_eq!(memory.end_position().samples(), 360_000);
assert_eq!(memory.markers().len(), 1);
assert!(history.redo_label().is_none());
assert_eq!(history.undo_label(), Some("Add marker"));
// 2927 bytes, the reload matches, clean until one more undo.
let path = "session_undo.superb.json";
memory.save(path)?;
history.mark_clean();
let bytes = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
let reloaded = Memory::load(path)?;
let identical = reloaded.snapshot() == memory.snapshot();
assert_eq!(bytes, 2927);
assert!(identical);
assert!(history.is_clean());
println!(
"saved {path} ({} bytes) and reloaded: identical = {}",
bytes, identical
);
println!("clean after save: {}", history.is_clean());
history.undo(&mut memory)?;
assert!(!history.is_clean());
println!("clean after one more undo: {}", history.is_clean());
Ok(())
}