diff --git a/Cargo.lock b/Cargo.lock index d38d0b0e..87cbb7e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,6 +212,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "clojure-reader" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe72db90a90a91de4a9fbd79542538caa0445ebdebcd3112589cab4c1e0e10b" +dependencies = [ + "ordered-float", +] + [[package]] name = "cmake" version = "0.1.50" @@ -680,6 +689,15 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "ordered-float" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ff2cf528c6c03d9ed653d6c4ce1dc0582dc4af309790ad92f07c1cd551b0be" +dependencies = [ + "num-traits", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -1032,6 +1050,7 @@ dependencies = [ "backtrace", "better-panic", "clap", + "clojure-reader", "crossterm", "fraction", "jack", diff --git a/Cargo.toml b/Cargo.toml index f5b03080..e4cd0e14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,4 @@ atomic_float = "1.0.0" fraction = "0.15.3" rlsf = "0.2.1" r8brain-rs = "0.3.5" +clojure-reader = "0.1.0" diff --git a/README.md b/README.md index e69de29b..7f961be3 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,13 @@ +TODO: + +* Focus transport to set BPM/sync/quant with `.,` +* Sample browser +* Envelope +* Stretch sample to PPM +* Move clip/track/scene +* Fix next/prev clip +* needs_update +* Buffer sequencer +* Buffer chain view +* Device ports +* LV2 GUI diff --git a/demos/project.edn b/demos/project.edn new file mode 100644 index 00000000..63036e75 --- /dev/null +++ b/demos/project.edn @@ -0,0 +1,67 @@ +(track { :name "Drums" :gain +0.0 } + + (sampler { :name "DrumKit1" :dir "/home/user/Lab/Music/pak" } + (sample { :midi 34 :name "808" :file "808.wav" }) + (sample { :midi 35 :name "KC1" :file "kik.wav" }) + (sample { :midi 36 :name "KC2" :file "kik2.wav" }) + (sample { :midi 38 :name "SN1" :file "sna.wav" }) + (sample { :midi 40 :name "SN2" :file "sna2.wav" }) + (sample { :midi 42 :name "HH1" :file "chh.wav" }) + (sample { :midi 44 :name "HH2" :file "chh2.wav" })) + + (phrase { :name "4 kicks" :beats 4 :steps 16 } + (:00 (36 128)) + (:04 (36 128)) + (:08 (36 128)) + (:12 (36 128))) + +) + + ;(phrase { + ;:name "5 kicks" + ;:beats 4 + ;} (:00 (36 128)) + ;(:04 (36 128)) + ;(:08 (36 128)) + ;(:12 (36 128)) + ;(:14 (36 128))) + + ;(phrase { + ;:name "D Beat" + ;:beats 4 + ;} (:00 (:44 :100) (:34 :100) (:35 :100)) + ;(:02 (:42 :100) ) + ;(:04 (:42 :080) (:38 :100) ) + ;(:06 (:44 :120) ) + ;(:08 (:42 :100) (:34 :100) (:35 :100)) + ;(:10 (:42 :100) (:34 :100) (:35 :100)) + ;(:12 (:44 :100) (:40 :100) ) + ;(:13 (:44 :100) ) + ;(:14 (:44 :100) ) + ;(:15 (:42 :100) )) + + ;(phrase { + ;:name "Garage" + ;:beats 4 + ;} (:00 (44 100) (36 100) (35 100)) + ;(:01 (44 100)) + ;(:02 (44 100) (35 100)) + ;(:03 (44 100)) + ;(:04 (44 100) (40 100)) + ;(:06 (44 100)) + ;(:07 (44 100) (34 100)) + ;(:09 (44 100)) + ;(:10 (44 100)) + ;(:11 (35 100) (36 100)) + ;(:12 (44 100) (40 100)) + ;(:14 (44 100)))) + +;(track "Bass" + ;(lv2-plugin "Odin2") + ;(phrase { :name "Empty" :beats 4 }) + ;(phrase { :name "Empty" :beats 4 })) + +;(track "Lead" + ;(lv2-plugin "Odin2") + ;(phrase { :name "Empty" :beats 4 }) + ;(phrase { :name "Empty" :beats 4 })) diff --git a/src/control.rs b/src/control.rs index 530daef6..dea34734 100644 --- a/src/control.rs +++ b/src/control.rs @@ -34,13 +34,13 @@ handle!(App |self, e| { }); const KEYMAP_FOCUS: &'static [KeyBinding] = keymap!(App { - [Tab, NONE, "focus_next", "focus next area", focus_next], - [Tab, SHIFT, "focus_prev", "focus previous area", focus_prev], - [Esc, NONE, "focus_exit", "unfocus", |app: &mut App|{ + [Tab, NONE, "focus_next", "focus next area", focus_next], + [Tab, SHIFT, "focus_prev", "focus previous area", focus_prev], + [Esc, NONE, "focus_exit", "unfocus", |app: &mut App|{ app.entered = false; Ok(true) }], - [Enter, NONE, "focus_enter", "activate item at cursor", |app: &mut App|{ + [Enter, NONE, "focus_enter", "activate item at cursor", |app: &mut App|{ app.entered = true; Ok(true) }], @@ -50,8 +50,8 @@ const KEYMAP: &'static [KeyBinding] = keymap!(App { [F(1), NONE, "help_toggle", "toggle help", |_: &mut App| {Ok(true)}], - [Up, NONE, "focus_prev", "focus previous area", focus_prev], - [Down, NONE, "focus_next", "focus next area", focus_next], + [Up, NONE, "focus_prev", "focus previous area", focus_prev], + [Down, NONE, "focus_next", "focus next area", focus_next], [Char(' '), NONE, "play_toggle", "play or pause", |app: &mut App| { app.toggle_play()?; @@ -175,13 +175,52 @@ const KEYMAP_GRID: &'static [KeyBinding] = keymap!(App { true } )], - [Char('.'), NONE, "grid_increment", "increment item at cursor", clip_next], - [Char(','), NONE, "grid_decrement", "decrement item at cursor", clip_prev], + [Char('.'), NONE, "grid_increment", "set next clip at cursor", |app: &mut App| { + Ok(true) + }], + [Char(','), NONE, "grid_decrement", "set previous clip at cursor", |app: &mut App| { + Ok(true) + }], [Char('`'), NONE, "grid_mode_switch", "switch the display mode", |app: &mut App| { app.grid_mode = !app.seq_mode; Ok(true) }], }); +fn clip_next (_: &mut App) -> Usually { Ok(true) } +//fn clip_next (state: &mut Launcher) -> Usually { + //if state.cursor.0 >= 1 && state.cursor.1 >= 1 { + //let scene_id = state.cursor.1 - 1; + //let clip_id = state.cursor.0 - 1; + //let scene = &mut state.scenes[scene_id]; + //scene.clips[clip_id] = match scene.clips[clip_id] { + //None => Some(0), + //Some(i) => if i >= state.tracks[clip_id].sequencer.phrases.len().saturating_sub(1) { + //None + //} else { + //Some(i + 1) + //} + //}; + //} + //Ok(true) +//} + +fn clip_prev (_: &mut App) -> Usually { Ok(true) } +//fn clip_prev (state: &mut Launcher) -> Usually { + //if state.cursor.0 >= 1 && state.cursor.1 >= 1 { + //let scene_id = state.cursor.1 - 1; + //let clip_id = state.cursor.0 - 1; + //let scene = &mut state.scenes[scene_id]; + //scene.clips[clip_id] = match scene.clips[clip_id] { + //None => Some(state.tracks[clip_id].sequencer.phrases.len().saturating_sub(1)), + //Some(i) => if i == 0 { + //None + //} else { + //Some(i - 1) + //} + //}; + //} + //Ok(true) +//} const KEYMAP_SEQUENCER: &'static [KeyBinding] = keymap!(App { [Up, NONE, "seq_cursor_up", "move cursor up", |app: &mut App| { @@ -257,46 +296,11 @@ fn increment (app: &mut App) -> Usually { Ok(false) } -fn clip_next (_: &mut App) -> Usually { Ok(true) } -//fn clip_next (state: &mut Launcher) -> Usually { - //if state.cursor.0 >= 1 && state.cursor.1 >= 1 { - //let scene_id = state.cursor.1 - 1; - //let clip_id = state.cursor.0 - 1; - //let scene = &mut state.scenes[scene_id]; - //scene.clips[clip_id] = match scene.clips[clip_id] { - //None => Some(0), - //Some(i) => if i >= state.tracks[clip_id].sequencer.phrases.len().saturating_sub(1) { - //None - //} else { - //Some(i + 1) - //} - //}; - //} - //Ok(true) -//} fn decrement (app: &mut App) -> Usually { Ok(false) } -fn clip_prev (_: &mut App) -> Usually { Ok(true) } -//fn clip_prev (state: &mut Launcher) -> Usually { - //if state.cursor.0 >= 1 && state.cursor.1 >= 1 { - //let scene_id = state.cursor.1 - 1; - //let clip_id = state.cursor.0 - 1; - //let scene = &mut state.scenes[scene_id]; - //scene.clips[clip_id] = match scene.clips[clip_id] { - //None => Some(state.tracks[clip_id].sequencer.phrases.len().saturating_sub(1)), - //Some(i) => if i == 0 { - //None - //} else { - //Some(i - 1) - //} - //}; - //} - //Ok(true) -//} - fn delete (app: &mut App) -> Usually { match app.section { AppSection::Grid => delete_track(app), diff --git a/src/edn.rs b/src/edn.rs new file mode 100644 index 00000000..50cd94d4 --- /dev/null +++ b/src/edn.rs @@ -0,0 +1,222 @@ +use crate::{core::*, model::*, App}; + +use clojure_reader::edn::Edn; + +impl App { + pub fn load_edn (&mut self, mut src: &str) { + loop { + match clojure_reader::edn::read(src) { + Ok((edn, rest)) => { + self.load_edn_one(edn); + if rest.len() > 0 { + src = rest; + } else { + break + } + }, + Err(e) => { + panic!("{e:?}"); + } + } + } + } + fn load_edn_one <'e> (&mut self, edn: Edn<'e>) { + match edn { + Edn::List(items) => { + let head = items.get(0); + match head { + Some(Edn::Symbol("track")) => Track::load_edn(self, &items[1..]), + _ => panic!("unexpected edn: {head:?}") + } + }, + _ => { + panic!("unexpected edn: {edn:?}"); + } + }; + } +} + +impl Track { + fn load_edn <'a, 'e> (app: &'a mut App, items: &[Edn<'e>]) -> Usually<&'a mut Self> { + let ppq = app.timebase.ppq() as usize; + let mut name = String::new(); + let mut gain = 0.0f64; + let mut devices: Vec = vec![]; + let mut phrases: Vec = vec![]; + for edn in items[1..].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::Double(g)) = map.get(&Edn::Key("gain")) { + gain = f64::from(*g) + } + }, + Edn::List(items) => match items.get(0) { + Some(Edn::Symbol("phrase")) => { + phrases.push(Phrase::load_edn(ppq, items)?) + }, + Some(Edn::Symbol("sampler")) => { + devices.push(Sampler::load_edn(items)?) + }, + Some(Edn::Symbol("lv2")) => { + devices.push(LV2Plugin::load_edn(items)?) + }, + None => panic!("empty list track {name}"), + _ => panic!("unexpected in track {name}: {:?}", items.get(0).unwrap()) + }, + _ => {} + } + } + app.add_track_with_cb(Some(name.as_str()), move|_, track|{ + for phrase in phrases { + track.phrases.push(phrase); + } + for device in devices { + track.add_device(device); + } + Ok(()) + }) + } +} + +impl Phrase { + fn load_edn <'e> (ppq: usize, items: &[Edn<'e>]) -> Usually { + let mut name = String::new(); + let mut beats = 0usize; + let mut steps = 0usize; + let mut data = BTreeMap::new(); + for edn in items[1..].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::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(items) => { + let time = (match items.get(0) { + Some(Edn::Symbol(text)) => text.parse::()?, + Some(Edn::Int(i)) => *i as f64, + Some(Edn::Double(f)) => f64::from(*f), + _ => panic!("unexpected in phrase {name}: {:?}", items.get(0)), + } * beats as f64 * ppq as f64 / steps as f64) as usize; + for edn in items[1..].iter() { + match edn { + Edn::List(items) => if let ( + Some(Edn::Int(key)), + Some(Edn::Int(vel)), + ) = ( + items.get(0), + items.get(1), + ) { + if !data.contains_key(&time) { + data.insert(time, vec![]); + } + data.get_mut(&time).unwrap().push(MidiMessage::NoteOn { + key: u7::from(*key as u8), + vel: u7::from(*vel as u8), + }); + } else { + panic!("unexpected list in phrase {name}") + }, + _ => panic!("unexpected in phrase {name}: {edn:?}") + } + } + }, + _ => panic!("unexpected in phrase {name}: {edn:?}"), + } + } + Ok(Self::new(&name, beats * ppq, Some(data))) + } +} + +impl Sampler { + fn load_edn <'e> (items: &[Edn<'e>]) -> Usually { + let mut name = String::new(); + let mut dir = String::new(); + let mut samples = BTreeMap::new(); + for edn in items[1..].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(n)) = map.get(&Edn::Key("dir")) { + dir = String::from(*n); + } + }, + Edn::List(items) => match items.get(0) { + Some(Edn::Symbol("sample")) => { + let (midi, sample) = Sample::load_edn(&items[1..])?; + if let Some(midi) = midi { + samples.insert(midi, sample); + } else { + panic!("sample without midi binding: {}", sample.name); + } + }, + _ => panic!("unexpected in sampler {name}: {items:?}") + }, + _ => panic!("unexpected in sampler {name}: {edn:?}") + } + } + Self::new(&name, Some(samples)) + } +} + +impl Sample { + fn load_edn <'e> (items: &[Edn<'e>]) -> Usually<(Option, Arc)> { + let mut name = String::new(); + let mut file = String::new(); + let mut midi = None; + let mut start = 0usize; + for edn in items[1..].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}"), + } + } + let (end, data) = read_sample_data(&file)?; + Ok((midi, Self::new(&name, start, end, data))) + } +} + +impl LV2Plugin { + fn load_edn <'e> (items: &[Edn<'e>]) -> Usually { + let mut name = String::new(); + let mut path = String::new(); + for edn in items[1..].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 sample {name}"), + } + } + Plugin::lv2(&name, &path) + } +} diff --git a/src/main.rs b/src/main.rs index e00dff29..53f5f12e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,11 +16,13 @@ pub mod core; pub mod model; pub mod view; pub mod jack; +pub mod edn; use crate::{core::*, model::*}; /// Application entrypoint. pub fn main () -> Usually<()> { + let mut app = App::default(); // Load config @@ -75,6 +77,8 @@ pub fn main () -> Usually<()> { state.jack = Some(jack); + 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([ diff --git a/src/model.rs b/src/model.rs index e33c3259..e55ff67c 100644 --- a/src/model.rs +++ b/src/model.rs @@ -9,7 +9,7 @@ pub mod track; pub use self::phrase::Phrase; pub use self::scene::Scene; pub use self::track::Track; -pub use self::sampler::{Sampler, Sample}; +pub use self::sampler::{Sampler, Sample, read_sample_data}; pub use self::mixer::Mixer; pub use self::plugin::{Plugin, PluginKind, lv2::LV2Plugin}; @@ -158,7 +158,7 @@ impl App { pub fn add_track_with_cb ( &mut self, name: Option<&str>, - init: impl Fn(&Client, &mut Track)->Usually<()>, + init: impl FnOnce(&Client, &mut Track)->Usually<()>, ) -> Usually<&mut Track> { let name = name.ok_or_else(||format!("Track {}", self.tracks.len() + 1))?; let mut track = Track::new(&name, self.client(), None, None)?; diff --git a/src/model/sampler.rs b/src/model/sampler.rs index d99d5848..e714855b 100644 --- a/src/model/sampler.rs +++ b/src/model/sampler.rs @@ -114,22 +114,28 @@ impl Sampler { /// Load sample from WAV and assign to MIDI note. #[macro_export] macro_rules! sample { - ($note:expr, $name:expr, $src:expr) => { - { - let mut channels: Vec> = vec![]; - for channel in wavers::Wav::from_path($src)?.channels() { - channels.push(channel); - } - let mut end = 0; - let mut data: Vec> = vec![]; - for samples in channels.iter() { - let channel = Vec::from(samples.as_ref()); - end = end.max(channel.len()); - data.push(channel); - } - (u7::from_int_lossy($note).into(), Sample::new($name, 0, end, data).into()) - } - }; + ($note:expr, $name:expr, $src:expr) => {{ + let (end, data) = read_sample_data($src)?; + ( + u7::from_int_lossy($note).into(), + Sample::new($name, 0, end, data).into() + ) + }}; +} + +pub fn read_sample_data (src: &str) -> Usually<(usize, Vec>)> { + let mut channels: Vec> = vec![]; + for channel in wavers::Wav::from_path(src)?.channels() { + channels.push(channel); + } + let mut end = 0; + let mut data: Vec> = vec![]; + for samples in channels.iter() { + let channel = Vec::from(samples.as_ref()); + end = end.max(channel.len()); + data.push(channel); + } + Ok((end, data)) } pub struct Sample { diff --git a/src/view.rs b/src/view.rs index 123b2838..03bd8554 100644 --- a/src/view.rs +++ b/src/view.rs @@ -48,7 +48,7 @@ render!(App |self, buf, area| { Style::default().green() } else { Style::default().green().dim() - }).draw(buf, Rect { x, y, width, height: chain.height }) + }).draw(buf, chain) } let phrase = self.draw_phrase(buf, Rect { x, y, width, height: height - height / 3 diff --git a/src/view/grid.rs b/src/view/grid.rs index 01c253e1..7cfc73a9 100644 --- a/src/view/grid.rs +++ b/src/view/grid.rs @@ -131,7 +131,7 @@ impl<'a> SceneGridViewVertical<'a> { (0 == self.cursor.0) && (index + 1 == self.cursor.1) ).bold()); "⯈".blit(self.buf, x, y, style); - scene.name.blit(self.buf, x + 1, y, style); + scene.name.blit(self.buf, x + 2, y, style); } } diff --git a/src/view/sequencer.rs b/src/view/sequencer.rs index 9ab2802a..c1d20b7a 100644 --- a/src/view/sequencer.rs +++ b/src/view/sequencer.rs @@ -155,11 +155,11 @@ mod horizontal { cell_bg_tick.set_style(bw); let mut cell_a = Cell::default(); - cell_a.set_char('▀'); + cell_a.set_char('▄'); cell_a.set_style(wh); let mut cell_b = Cell::default(); - cell_b.set_char('▄'); + cell_b.set_char('▀'); cell_b.set_style(wh); let mut cell_ab = Cell::default();