diff --git a/README.md b/README.md index 7f961be3..4a0682b6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ +# tek + +Tek is a MIDI sequencer, sampler, and plugin host for the Linux terminal. + +## Requirements + +* Linux +* Rust toolchain +* JACK or Pipewire + +## Recommended + +* MIDI controller +* Samples + TODO: * Focus transport to set BPM/sync/quant with `.,` diff --git a/demos/project.edn b/demos/project.edn index a3fafcd4..0da335b2 100644 --- a/demos/project.edn +++ b/demos/project.edn @@ -1,5 +1,12 @@ (bpm 150) +(scene { :name "Intro" } _ 0 _ _) +(scene { :name "Hook" } 0 1 0 _) +(scene { :name "Verse" } 2 2 1 _) +(scene { :name "Chorus" } 1 3 2 _) +(scene { :name "Bridge" } 3 4 3 _) +(scene { :name "Outro" } _ 1 4 _) + (track { :name "Drums" :gain +0.0 } (phrase { :name "4 kicks" :beats 4 :steps 16 } (:00 (36 128)) diff --git a/src/edn.rs b/src/edn.rs index fd1c55b4..df094b25 100644 --- a/src/edn.rs +++ b/src/edn.rs @@ -2,6 +2,17 @@ use crate::{core::*, model::*, App}; use clojure_reader::{edn::{read, Edn}, error::Error as EdnError}; +macro_rules! edn { + ($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => { + match $edn { $($pat => $expr),* } + }; + ($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => { + for $edn in $args { + edn!($edn { $($pat => $expr),* }) + } + }; +} + impl App { pub fn load_edn (&mut self, mut src: &str) -> Usually<()> { loop { @@ -27,11 +38,7 @@ impl App { fn load_edn_one <'e> (&mut self, edn: Edn<'e>) -> Usually<()> { match edn { Edn::List(items) => { - let head = items.get(0); - match head { - Some(Edn::Symbol("track")) => { - Track::load_edn(self, &items[1..])?; - }, + match items.get(0) { Some(Edn::Symbol("bpm")) => { match items.get(1) { Some(Edn::Int(b)) => self.timebase.set_bpm(*b as f64), @@ -39,7 +46,13 @@ impl App { _ => panic!("unspecified bpm") } }, - _ => panic!("unexpected edn: {head:?}") + Some(Edn::Symbol("scene")) => { + Scene::load_edn(self, &items[1..])?; + }, + Some(Edn::Symbol("track")) => { + Track::load_edn(self, &items[1..])?; + }, + _ => panic!("unexpected edn: {:?}", items.get(0)) } }, _ => { @@ -50,6 +63,35 @@ impl App { } } +impl Scene { + fn load_edn <'a, 'e> (app: &'a mut App, args: &[Edn<'e>]) -> Usually<&'a mut Self> { + let mut name = None; + let mut clips = vec![]; + edn!(edn in args { + Edn::Map(map) => { + let key = map.get(&Edn::Key(":name")); + if let Some(Edn::Str(n)) = key { + name = Some(String::from(*n)); + } else { + panic!("unexpected key in scene '{name:?}': {key:?}") + } + }, + Edn::Symbol("_") => { + clips.push(None); + }, + Edn::Int(i) => { + clips.push(Some(*i as usize)); + }, + _ => panic!("unexpected in scene '{name:?}': {edn:?}") + }); + app.add_scene_with_clips(name.as_deref(), &clips) + //for edn in args { + //match end { + //} + //} + } +} + impl Track { fn load_edn <'a, 'e> (app: &'a mut App, args: &[Edn<'e>]) -> Usually<&'a mut Self> { let ppq = app.timebase.ppq() as usize; @@ -57,32 +99,30 @@ impl Track { let mut gain = 0.0f64; let mut devices: Vec = vec![]; let mut phrases: Vec = vec![]; - for edn in args { - match edn { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Double(g)) = map.get(&Edn::Key(":gain")) { - gain = f64::from(*g) - } + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Double(g)) = map.get(&Edn::Key(":gain")) { + gain = f64::from(*g) + } + }, + Edn::List(args) => match args.get(0) { + Some(Edn::Symbol("phrase")) => { + phrases.push(Phrase::load_edn(ppq, &args[1..])?) }, - Edn::List(args) => match args.get(0) { - Some(Edn::Symbol("phrase")) => { - phrases.push(Phrase::load_edn(ppq, &args[1..])?) - }, - Some(Edn::Symbol("sampler")) => { - devices.push(Sampler::load_edn(&args[1..])?) - }, - Some(Edn::Symbol("lv2")) => { - devices.push(LV2Plugin::load_edn(&args[1..])?) - }, - None => panic!("empty list track {name}"), - _ => panic!("unexpected in track {name}: {:?}", args.get(0).unwrap()) + Some(Edn::Symbol("sampler")) => { + devices.push(Sampler::load_edn(&args[1..])?) }, - _ => {} - } - } + Some(Edn::Symbol("lv2")) => { + devices.push(LV2Plugin::load_edn(&args[1..])?) + }, + None => panic!("empty list track {name}"), + _ => panic!("unexpected in track {name}: {:?}", args.get(0).unwrap()) + }, + _ => {} + }); let (left, right) = (app.audio_out(0), app.audio_out(1)); app.add_track_with_cb(Some(name.as_str()), move|_, track|{ for phrase in phrases { @@ -113,54 +153,52 @@ impl Phrase { let mut beats = 0usize; let mut steps = 0usize; let mut data = BTreeMap::new(); - for edn in args { - match edn { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Int(b)) = map.get(&Edn::Key(":beats")) { + beats = *b as usize; + } + if let Some(Edn::Int(s)) = map.get(&Edn::Key(":steps")) { + steps = *s as usize; + } + }, + Edn::List(args) => { + let time = (match args.get(0) { + Some(Edn::Key(text)) => text[1..].parse::()?, + Some(Edn::Int(i)) => *i as f64, + Some(Edn::Double(f)) => f64::from(*f), + _ => panic!("unexpected in phrase '{name}': {:?}", args.get(0)), + } * beats as f64 * ppq as f64 / steps as f64) as usize; + for edn in args[1..].iter() { + match edn { + Edn::List(args) => if let ( + Some(Edn::Int(key)), + Some(Edn::Int(vel)), + ) = ( + args.get(0), + args.get(1), + ) { + if !data.contains_key(&time) { + data.insert(time, vec![]); + } + let (key, vel) = ( + u7::from((*key as u8).min(127)), + u7::from((*vel as u8).min(127)) + ); + data.get_mut(&time).unwrap() + .push(MidiMessage::NoteOn { key, vel }) + } else { + panic!("unexpected list in phrase '{name}'") + }, + _ => panic!("unexpected in phrase '{name}': {edn:?}") } - if let Some(Edn::Int(b)) = map.get(&Edn::Key(":beats")) { - beats = *b as usize; - } - if let Some(Edn::Int(s)) = map.get(&Edn::Key(":steps")) { - steps = *s as usize; - } - }, - Edn::List(args) => { - let time = (match args.get(0) { - Some(Edn::Key(text)) => text[1..].parse::()?, - Some(Edn::Int(i)) => *i as f64, - Some(Edn::Double(f)) => f64::from(*f), - _ => panic!("unexpected in phrase '{name}': {:?}", args.get(0)), - } * beats as f64 * ppq as f64 / steps as f64) as usize; - for edn in args[1..].iter() { - match edn { - Edn::List(args) => if let ( - Some(Edn::Int(key)), - Some(Edn::Int(vel)), - ) = ( - args.get(0), - args.get(1), - ) { - if !data.contains_key(&time) { - data.insert(time, vec![]); - } - let (key, vel) = ( - u7::from((*key as u8).min(127)), - u7::from((*vel as u8).min(127)) - ); - data.get_mut(&time).unwrap() - .push(MidiMessage::NoteOn { key, vel }) - } else { - panic!("unexpected list in phrase '{name}'") - }, - _ => panic!("unexpected in phrase '{name}': {edn:?}") - } - } - }, - _ => panic!("unexpected in phrase '{name}': {edn:?}"), - } - } + } + }, + _ => panic!("unexpected in phrase '{name}': {edn:?}"), + }); Ok(Self::new(&name, beats * ppq, Some(data))) } } @@ -170,30 +208,28 @@ impl Sampler { let mut name = String::new(); let mut dir = String::new(); let mut samples = BTreeMap::new(); - for edn in args { - match edn { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) { - dir = String::from(*n); + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) { + dir = String::from(*n); + } + }, + Edn::List(args) => match args.get(0) { + Some(Edn::Symbol("sample")) => { + let (midi, sample) = Sample::load_edn(&dir, &args[1..])?; + if let Some(midi) = midi { + samples.insert(midi, sample); + } else { + panic!("sample without midi binding: {}", sample.name); } }, - Edn::List(args) => match args.get(0) { - Some(Edn::Symbol("sample")) => { - let (midi, sample) = Sample::load_edn(&dir, &args[1..])?; - if let Some(midi) = midi { - samples.insert(midi, sample); - } else { - panic!("sample without midi binding: {}", sample.name); - } - }, - _ => panic!("unexpected in sampler {name}: {args:?}") - }, - _ => panic!("unexpected in sampler {name}: {edn:?}") - } - } + _ => panic!("unexpected in sampler {name}: {args:?}") + }, + _ => panic!("unexpected in sampler {name}: {edn:?}") + }); Self::new(&name, Some(samples)) } } @@ -204,25 +240,23 @@ impl Sample { let mut file = String::new(); let mut midi = None; let mut start = 0usize; - for edn in args.iter() { - match edn { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) { - file = String::from(*f); - } - if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) { - start = *i as usize; - } - if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) { - midi = Some(u7::from(*m as u8)); - } - }, - _ => panic!("unexpected in sample {name}"), - } - } + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) { + file = String::from(*f); + } + if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) { + start = *i as usize; + } + if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) { + midi = Some(u7::from(*m as u8)); + } + }, + _ => panic!("unexpected in sample {name}"), + }); let (end, data) = read_sample_data(&format!("{dir}/{file}"))?; Ok((midi, Self::new(&name, start, end, data))) } @@ -232,19 +266,17 @@ impl LV2Plugin { fn load_edn <'e> (args: &[Edn<'e>]) -> Usually { let mut name = String::new(); let mut path = String::new(); - for edn in args.iter() { - match edn { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Str(p)) = map.get(&Edn::Key(":path")) { - path = String::from(*p); - } - }, - _ => panic!("unexpected in lv2 '{name}'"), - } - } + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Str(p)) = map.get(&Edn::Key(":path")) { + path = String::from(*p); + } + }, + _ => panic!("unexpected in lv2 '{name}'"), + }); Plugin::lv2(&name, &path) } } diff --git a/src/main.rs b/src/main.rs index 14240bf4..b8f519bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,9 +22,8 @@ use crate::{core::*, model::*}; /// Application entrypoint. pub fn main () -> Usually<()> { - + // Construct app let mut app = App::default(); - // Load config let xdg = Arc::new(microxdg::XdgApp::new("tek")?); app.xdg = Some(xdg.clone()); @@ -33,9 +32,7 @@ pub fn main () -> Usually<()> { } let midi_from = ["nanoKEY Studio.*capture.*"]; let audio_into = ["Komplete.+:playback_FL", "Komplete.+:playback_FR"]; - - // Init - let ppq = app.timebase.ppq() as usize; + // Init view app.track_cursor = 1; app.scene_cursor = 1; app.note_start = 12; @@ -45,13 +42,11 @@ pub fn main () -> Usually<()> { // Start main loop app.run(Some(|app: Arc>|{ let mut state = app.write().unwrap(); - // Start JACK and setup device graph let jack = jack_run("tek", &app)?; let client = jack.as_client(); state.transport = Some(client.transport()); state.midi_in = Some(client.register_port("midi-in", MidiIn)?); - let _ = midi_from .iter() .map(|name|client @@ -65,7 +60,6 @@ pub fn main () -> Usually<()> { }) .collect::>()) .collect::>()?; - state.audio_outs = audio_into .iter() .map(|name|client @@ -76,169 +70,9 @@ pub fn main () -> Usually<()> { .filter_map(|x|x) .map(Arc::new) .collect(); - state.jack = Some(jack); - + // Load project state.load_edn(include_str!("../demos/project.edn"))?; - - //state.add_track_with_cb(Some("Drums"), |_, track|{ - - //track.add_device_with_cb(Sampler::new("Sampler", Some(BTreeMap::from([ - //sample!(34, "808", "/home/user/Lab/Music/pak/808.wav"), - //sample!(35, "Kick1", "/home/user/Lab/Music/pak/kik.wav"), - //sample!(36, "Kick2", "/home/user/Lab/Music/pak/kik2.wav"), - //sample!(38, "Snare1", "/home/user/Lab/Music/pak/sna.wav"), - //sample!(40, "Snare2", "/home/user/Lab/Music/pak/sna2.wav"), - //sample!(42, "Hihat", "/home/user/Lab/Music/pak/chh.wav"), - //sample!(44, "Hihat", "/home/user/Lab/Music/pak/chh2.wav"), - //])))?, |track, device|{ - //device.connect_midi_in(0, &track.midi_out.clone_unowned())?; - //if let Some(Some(left)) = audio_outs.get(0) { - //device.connect_audio_out(0, left)?; - //} - //if let Some(Some(right)) = audio_outs.get(0) { - //device.connect_audio_out(1, right)?; - //} - //Ok(()) - //})?; - - ////track.add_device_with_cb(Plugin::lv2( - ////"Panagement", - ////"file:///home/user/.lv2/Auburn Sounds Panagement 2.lv2" - ////)?, |track, device|{ - ////device.connect_audio_in(0, &track.devices[0].audio_outs()?[0])?; - ////device.connect_audio_in(0, &track.devices[0].audio_outs()?[1])?; - ////if let Some(Some(left)) = audio_outs.get(0) { - ////device.connect_audio_out(0, left)?; - ////} - ////if let Some(Some(right)) = audio_outs.get(0) { - ////device.connect_audio_out(1, right)?; - ////} - ////Ok(()) - ////})?; - - //track.sequence = Some(1); // FIXME - - //track.add_phrase("4 kicks", ppq * 4, Some(phrase! { - //00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - //04 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - //08 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - //12 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - //})); - //track.add_phrase("5 kicks", ppq * 4, Some(phrase! { - //00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - //04 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - //08 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - //12 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - //14 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - //})); - //track.add_phrase("D-Beat", ppq * 4, Some(phrase! { - //00 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - //02 * ppq/4 => MidiMessage::NoteOn { key: 42.into(), vel: 100.into() }, - //04 * ppq/4 => MidiMessage::NoteOn { key: 42.into(), vel: 100.into() }, - //06 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - //08 * ppq/4 => MidiMessage::NoteOn { key: 42.into(), vel: 100.into() }, - //10 * ppq/4 => MidiMessage::NoteOn { key: 42.into(), vel: 100.into() }, - //12 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - //13 * ppq/4 => MidiMessage::NoteOn { key: 42.into(), vel: 100.into() }, - //14 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - //15 * ppq/4 => MidiMessage::NoteOn { key: 42.into(), vel: 100.into() }, - //00 * ppq/4 => MidiMessage::NoteOn { key: 34.into(), vel: 100.into() }, - //04 * ppq/4 => MidiMessage::NoteOn { key: 38.into(), vel: 100.into() }, - //08 * ppq/4 => MidiMessage::NoteOn { key: 34.into(), vel: 100.into() }, - //10 * ppq/4 => MidiMessage::NoteOn { key: 35.into(), vel: 100.into() }, - //12 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - //})); - //track.add_phrase("Garage", ppq * 4, Some(phrase! { - //00 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - //01 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - //02 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - //03 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - //04 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - //06 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - //07 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - //09 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - //10 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - //12 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - //14 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - - //00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - //00 * ppq/4 => MidiMessage::NoteOn { key: 35.into(), vel: 100.into() }, - //02 * ppq/4 => MidiMessage::NoteOn { key: 34.into(), vel: 100.into() }, - //07 * ppq/4 => MidiMessage::NoteOn { key: 34.into(), vel: 100.into() }, - //04 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - //11 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - //11 * ppq/4 => MidiMessage::NoteOn { key: 35.into(), vel: 100.into() }, - //12 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - //})); - //Ok(()) - //})?; - - //state.add_track_with_cb(Some("Bass"), |_, track|{ - //track.add_device_with_cb(Plugin::lv2( - //"Odin2", - //"file:///home/user/.lv2/Odin2.lv2" - //)?, |track, device|{ - //device.connect_midi_in(0, &track.midi_out.clone_unowned())?; - //if let Some(Some(left)) = audio_outs.get(0) { - //device.connect_audio_out(0, left)?; - //} - //if let Some(Some(right)) = audio_outs.get(0) { - //device.connect_audio_out(1, right)?; - //} - //Ok(()) - //})?; - //track.sequence = Some(0); // FIXME - //track.add_phrase("Offbeat", ppq * 4, Some(phrase! { - ////00 * ppq/4 => MidiMessage::NoteOff { key: 40.into(), vel: 100.into() }, - ////02 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - ////04 * ppq/4 => MidiMessage::NoteOff { key: 40.into(), vel: 100.into() }, - ////06 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - ////08 * ppq/4 => MidiMessage::NoteOff { key: 40.into(), vel: 100.into() }, - ////10 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - ////12 * ppq/4 => MidiMessage::NoteOff { key: 40.into(), vel: 100.into() }, - ////14 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - //})); - //track.add_phrase("Custom1", ppq * 4, None); - //track.add_phrase("Custom2", ppq * 4, None); - //track.add_phrase("Custom3", ppq * 4, None); - //track.add_phrase("Custom4", ppq * 4, None); - //Ok(()) - //})?; - - //state.add_track_with_cb(Some("Lead"), |_, track|{ - //track.add_device_with_cb(Plugin::lv2( - //"Odin2", - //"file:///home/user/.lv2/Odin2.lv2" - //)?, |track, device|{ - //device.connect_midi_in(0, &track.midi_out.clone_unowned())?; - //if let Some(Some(left)) = audio_outs.get(0) { - //device.connect_audio_out(0, left)?; - //} - //if let Some(Some(right)) = audio_outs.get(0) { - //device.connect_audio_out(1, right)?; - //} - //Ok(()) - //})?; - //track.sequence = Some(0); // FIXME - //track.add_phrase("Custom0", ppq * 4, None); - //track.add_phrase("Custom1", ppq * 4, None); - //track.add_phrase("Custom2", ppq * 4, None); - //track.add_phrase("Custom3", ppq * 4, None); - //track.add_phrase("Custom4", ppq * 4, None); - //Ok(()) - //})?; - - state.scenes = vec![ - Scene::new("Intro", vec![None, Some(0), None, None]), - Scene::new("Hook", vec![Some(0), Some(1), Some(0), None]), - Scene::new("Verse", vec![Some(2), Some(2), Some(1), None]), - Scene::new("Chorus", vec![Some(1), Some(3), Some(2), None]), - Scene::new("Bridge", vec![Some(3), Some(4), Some(3), None]), - Scene::new("Outro", vec![None, Some(1), Some(4), None]), - ]; - Ok(()) })) - } diff --git a/src/model.rs b/src/model.rs index 71145c91..cc0b9980 100644 --- a/src/model.rs +++ b/src/model.rs @@ -185,12 +185,25 @@ impl App { self.tracks.get_mut(id).map(|t|(id, t)) } } } + pub fn new_scene_name (&self) -> String { + format!("Scene {}", self.scenes.len() + 1) + } pub fn add_scene (&mut self, name: Option<&str>) -> Usually<&mut Scene> { - let name = name.ok_or_else(||format!("Scene {}", self.scenes.len() + 1))?; + let name = name.ok_or_else(||self.new_scene_name())?; self.scenes.push(Scene::new(&name, vec![])); self.scene_cursor = self.scenes.len(); Ok(&mut self.scenes[self.scene_cursor - 1]) } + pub fn add_scene_with_clips ( + &mut self, + name: Option<&str>, + clips: &[Option] + ) -> Usually<&mut Scene> { + let name = name.ok_or_else(||self.new_scene_name())?; + self.scenes.push(Scene::new(&name, Vec::from(clips))); + self.scene_cursor = self.scenes.len(); + Ok(&mut self.scenes[self.scene_cursor - 1]) + } pub fn scene (&self) -> Option<(usize, &Scene)> { match self.scene_cursor { 0 => None, _ => { let id = self.scene_cursor as usize - 1;