diff --git a/config/config_arranger.edn b/config/config_arranger.edn new file mode 100644 index 00000000..cd711344 --- /dev/null +++ b/config/config_arranger.edn @@ -0,0 +1,25 @@ +(name "Arranger") + +(info "A session grid.") + +(view + (bsp/a :modal + (bsp/s (fixed/y 1 :transport) + (bsp/n (fixed/y 1 :status) + (fill/xy (bsp/a + (fill/xy (align/e :pool)) + :arranger)))))) + +(keys + (layer-if :mode-pool-import "./keys_pool_file.edn") + (layer-if :mode-pool-export "./keys_pool_file.edn") + (layer-if :mode-pool-rename "./keys_clip_rename.edn") + (layer-if :mode-pool-length "./keys_clip_length.edn") + (layer "./keys_global.edn") + (layer-if :mode-editor "./keys_editor.edn") + (layer-if :mode-clip "./keys_clip.edn") + (layer-if :mode-track "./keys_track.edn") + (layer-if :mode-scene "./keys_scene.edn") + (layer-if :mode-mix "./keys_mix.edn") + (layer "./keys_clock.edn") + (layer "./keys_arranger.edn")) diff --git a/config/config_groovebox.edn b/config/config_groovebox.edn new file mode 100644 index 00000000..c3815b89 --- /dev/null +++ b/config/config_groovebox.edn @@ -0,0 +1,22 @@ +(name "Arranger") + +(info "A sequencer with built-in sampler.") + +(view + (bsp/a :modal + (bsp/s (fixed/y 1 :transport) + (bsp/n (fixed/y 1 :status) + (bsp/n (fixed/y 5 :sample-viewer) + (bsp/w (fixed/x :w-sidebar :pool) + (bsp/e :samples-keys + (fill/y :editor)))))))) + +(keys + (layer-if :mode-pool-import "./keys_pool_file.edn") + (layer-if :mode-pool-export "./keys_pool_file.edn") + (layer-if :mode-pool-rename "./keys_clip_rename.edn") + (layer-if :mode-pool-length "./keys_clip_length.edn") + (layer "./keys_global.edn") + (layer-if :mode-editor "./keys_editor.edn") + (layer "./keys_clock.edn") + (layer "./keys_arranger.edn")) diff --git a/config/config_sampler.edn b/config/config_sampler.edn new file mode 100644 index 00000000..4d8fd6cb --- /dev/null +++ b/config/config_sampler.edn @@ -0,0 +1,13 @@ +(name "Sampler") + +(info "A sampling soundboard.") + +(view + (bsp/a :modal + (bsp/s (fixed/y 1 :transport) + (bsp/n (fixed/y 1 :status) + (fill/xy :samples-grid))))) + +(keys + (layer "./keys_global.edn") + (layer "./keys_sampler.edn")) diff --git a/config/config_sequencer.edn b/config/config_sequencer.edn new file mode 100644 index 00000000..1c1b6dfd --- /dev/null +++ b/config/config_sequencer.edn @@ -0,0 +1,21 @@ +(name "Sequencer") + +(info "A MIDI sequencer.") + +(view + (bsp/a :modal + (bsp/s (fixed/y 1 :transport) + (bsp/n (fixed/y 1 :status) + (fill/xy (bsp/a + (fill/xy (align/e :pool)) + :editor))))) + +(keys + (layer-if :mode-pool-import "./keys_pool_file.edn") + (layer-if :mode-pool-export "./keys_pool_file.edn") + (layer-if :mode-pool-rename "./keys_clip_rename.edn") + (layer-if :mode-pool-length "./keys_clip_length.edn") + (layer "./keys_global.edn") + (layer "./keys_editor.edn") + (layer "./keys_clock.edn") + (layer "./keys_arranger.edn")) diff --git a/config/config_transport.edn b/config/config_transport.edn new file mode 100644 index 00000000..00f28567 --- /dev/null +++ b/config/config_transport.edn @@ -0,0 +1,9 @@ +(name "Transport") + +(info "A JACK transport controller.") + +(view :transport) + +(keys + (layer "./keys_global.edn") + (layer "./keys_clock.edn")) diff --git a/config/view_arranger.edn b/config/view_arranger.edn deleted file mode 100644 index a1074d30..00000000 --- a/config/view_arranger.edn +++ /dev/null @@ -1,6 +0,0 @@ -(bsp/a :modal - (bsp/s (fixed/y 1 :transport) - (bsp/n (fixed/y 1 :status) - (fill/xy (bsp/a - (fill/xy (align/e :pool)) - :arranger))))) diff --git a/config/view_groovebox.edn b/config/view_groovebox.edn deleted file mode 100644 index d7c94a3b..00000000 --- a/config/view_groovebox.edn +++ /dev/null @@ -1,7 +0,0 @@ -(bsp/a :modal - (bsp/s (fixed/y 1 :transport) - (bsp/n (fixed/y 1 :status) - (bsp/n (fixed/y 5 :sample-viewer) - (bsp/w (fixed/x :w-sidebar :pool) - (bsp/e :samples-keys - (fill/y :editor))))))) diff --git a/config/view_sampler.edn b/config/view_sampler.edn deleted file mode 100644 index 6cad9cdc..00000000 --- a/config/view_sampler.edn +++ /dev/null @@ -1,4 +0,0 @@ -(bsp/a :modal - (bsp/s (fixed/y 1 :transport) - (bsp/n (fixed/y 1 :status) - (fill/xy :samples-grid)))) diff --git a/config/view_sequencer.edn b/config/view_sequencer.edn deleted file mode 100644 index d1ee923e..00000000 --- a/config/view_sequencer.edn +++ /dev/null @@ -1,6 +0,0 @@ -(bsp/a :modal - (bsp/s (fixed/y 1 :transport) - (bsp/n (fixed/y 1 :status) - (fill/xy (bsp/a - (fill/xy (align/e :pool)) - :editor))))) diff --git a/config/view_transport.edn b/config/view_transport.edn deleted file mode 100644 index c108e094..00000000 --- a/config/view_transport.edn +++ /dev/null @@ -1 +0,0 @@ -:transport diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index 960987ab..08e4b5a0 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -1,4 +1,5 @@ use crate::*; +use std::path::PathBuf; type MaybeClip = Option>>; @@ -31,8 +32,50 @@ expose!([self: Tek] (":scene-prev" self.selected.scene_prev()) (":track-next" self.selected.track_next(self.tracks.len())) (":track-prev" self.selected.track_prev()))); +provide!(bool: |self: MidiPool| {}); +provide!(MidiClip: |self: MidiPool| { + ":new-clip" => self.new_clip(), + ":cloned-clip" => self.cloned_clip(), +}); +provide!(PathBuf: |self: MidiPool| {}); +provide!(Arc: |self: MidiPool| {}); +provide!(usize: |self: MidiPool| { + ":current" => 0, + ":after" => 0, + ":previous" => 0, + ":next" => 0 +}); +provide!(ItemColor: |self: MidiPool| { + ":random-color" => ItemColor::random() +}); +provide!(bool: |self: MidiEditor| { + ":true" => true, + ":false" => false, + ":time-lock" => self.time_lock().get(), + ":time-lock-toggle" => !self.time_lock().get(), +}); +provide!(usize: |self: MidiEditor| { + ":note-length" => self.note_len(), + ":note-pos" => self.note_pos(), + ":note-pos-next" => self.note_pos() + 1, + ":note-pos-prev" => self.note_pos().saturating_sub(1), + ":note-pos-next-octave" => self.note_pos() + 12, + ":note-pos-prev-octave" => self.note_pos().saturating_sub(12), + ":note-len" => self.note_len(), + ":note-len-next" => self.note_len() + 1, + ":note-len-prev" => self.note_len().saturating_sub(1), + ":note-range" => self.note_axis().get(), + ":note-range-prev" => self.note_axis().get() + 1, + ":note-range-next" => self.note_axis().get().saturating_sub(1), + ":time-pos" => self.time_pos(), + ":time-pos-next" => self.time_pos() + self.time_zoom().get(), + ":time-pos-prev" => self.time_pos().saturating_sub(self.time_zoom().get()), + ":time-zoom" => self.time_zoom().get(), + ":time-zoom-next" => self.time_zoom().get() + 1, + ":time-zoom-prev" => self.time_zoom().get().saturating_sub(1).max(1), +}); -handle!(TuiIn: |self: Tek, input|Ok(if let Some(command) = self.keys.command(self, input) { +handle!(TuiIn: |self: Tek, input|Ok(if let Some(command) = self.config.keys.command(self, input) { let undo = command.execute(self)?; if let Some(undo) = undo { self.history.push(undo); @@ -193,3 +236,309 @@ fn delegate_to_pool (app: &mut Tek, cmd: PoolCommand) -> Perhaps { None }) } + +#[derive(Clone, PartialEq, Debug)] pub enum PoolCommand { + /// Toggle visibility of pool + Show(bool), + /// Select a clip from the clip pool + Select(usize), + /// Rename a clip + Rename(ClipRenameCommand), + /// Change the length of a clip + Length(ClipLengthCommand), + /// Import from file + Import(FileBrowserCommand), + /// Export to file + Export(FileBrowserCommand), + /// Update the contents of the clip pool + Clip(PoolClipCommand), +} + +atom_command!(PoolCommand: |state: MidiPool| { + ("show" [a: bool] Some(Self::Show(a.expect("no flag")))) + ("select" [i: usize] Some(Self::Select(i.expect("no index")))) + ("rename" [,..a] ClipRenameCommand::try_from_expr(state, a).map(Self::Rename)) + ("length" [,..a] ClipLengthCommand::try_from_expr(state, a).map(Self::Length)) + ("import" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Import)) + ("export" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Export)) + ("clip" [,..a] PoolClipCommand::try_from_expr(state, a).map(Self::Clip)) +}); + +command!(|self: PoolCommand, state: MidiPool|{ + use PoolCommand::*; + match self { + Rename(ClipRenameCommand::Begin) => { state.begin_clip_rename(); None } + Rename(command) => command.delegate(state, Rename)?, + Length(ClipLengthCommand::Begin) => { state.begin_clip_length(); None }, + Length(command) => command.delegate(state, Length)?, + Import(FileBrowserCommand::Begin) => { state.begin_import()?; None }, + Import(command) => command.delegate(state, Import)?, + Export(FileBrowserCommand::Begin) => { state.begin_export()?; None }, + Export(command) => command.delegate(state, Export)?, + Clip(command) => command.execute(state)?.map(Clip), + Show(visible) => { state.visible = visible; Some(Self::Show(!visible)) }, + Select(clip) => { state.set_clip_index(clip); None }, + } +}); + +#[derive(Clone, Debug, PartialEq)] pub enum PoolClipCommand { + Add(usize, MidiClip), + Delete(usize), + Swap(usize, usize), + Import(usize, PathBuf), + Export(usize, PathBuf), + SetName(usize, Arc), + SetLength(usize, usize), + SetColor(usize, ItemColor), +} + +impl Command for PoolClipCommand { + fn execute (self, model: &mut T) -> Perhaps { + use PoolClipCommand::*; + Ok(match self { + Add(mut index, clip) => { + let clip = Arc::new(RwLock::new(clip)); + let mut clips = model.clips_mut(); + if index >= clips.len() { + index = clips.len(); + clips.push(clip) + } else { + clips.insert(index, clip); + } + Some(Self::Delete(index)) + }, + Delete(index) => { + let clip = model.clips_mut().remove(index).read().unwrap().clone(); + Some(Self::Add(index, clip)) + }, + Swap(index, other) => { + model.clips_mut().swap(index, other); + Some(Self::Swap(index, other)) + }, + Import(index, path) => { + let bytes = std::fs::read(&path)?; + let smf = Smf::parse(bytes.as_slice())?; + let mut t = 0u32; + let mut events = vec![]; + for track in smf.tracks.iter() { + for event in track.iter() { + t += event.delta.as_int(); + if let TrackEventKind::Midi { channel, message } = event.kind { + events.push((t, channel.as_int(), message)); + } + } + } + let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None); + for event in events.iter() { + clip.notes[event.0 as usize].push(event.2); + } + Self::Add(index, clip).execute(model)? + }, + Export(_index, _path) => { + todo!("export clip to midi file"); + }, + SetName(index, name) => { + let clip = &mut model.clips_mut()[index]; + let old_name = clip.read().unwrap().name.clone(); + clip.write().unwrap().name = name; + Some(Self::SetName(index, old_name)) + }, + SetLength(index, length) => { + let clip = &mut model.clips_mut()[index]; + let old_len = clip.read().unwrap().length; + clip.write().unwrap().length = length; + Some(Self::SetLength(index, old_len)) + }, + SetColor(index, color) => { + let mut color = ItemTheme::from(color); + std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color); + Some(Self::SetColor(index, color.base)) + }, + }) + } +} + +atom_command!(ClipRenameCommand: |state: MidiPool| { + ("begin" [] Some(Self::Begin)) + ("cancel" [] Some(Self::Cancel)) + ("confirm" [] Some(Self::Confirm)) + ("set" [n: Arc] Some(Self::Set(n.expect("no name")))) +}); +atom_command!(ClipLengthCommand: |state: MidiPool| { + ("begin" [] Some(Self::Begin)) + ("cancel" [] Some(Self::Cancel)) + ("next" [] Some(Self::Next)) + ("prev" [] Some(Self::Prev)) + ("inc" [] Some(Self::Inc)) + ("dec" [] Some(Self::Dec)) + ("set" [l: usize] Some(Self::Set(l.expect("no length")))) +}); +atom_command!(FileBrowserCommand: |state: MidiPool| { + ("begin" [] Some(Self::Begin)) + ("cancel" [] Some(Self::Cancel)) + ("confirm" [] Some(Self::Confirm)) + ("select" [i: usize] Some(Self::Select(i.expect("no index")))) + ("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path")))) + ("filter" [f: Arc] Some(Self::Filter(f.expect("no filter")))) +}); +atom_command!(MidiEditCommand: |state: MidiEditor| { + ("note/append" [] Some(Self::AppendNote)) + ("note/put" [] Some(Self::PutNote)) + ("note/del" [] Some(Self::DelNote)) + ("note/pos" [a: usize] Some(Self::SetNoteCursor(a.expect("no note cursor")))) + ("note/len" [a: usize] Some(Self::SetNoteLength(a.expect("no note length")))) + ("time/pos" [a: usize] Some(Self::SetTimeCursor(a.expect("no time cursor")))) + ("time/zoom" [a: usize] Some(Self::SetTimeZoom(a.expect("no time zoom")))) + ("time/lock" [a: bool] Some(Self::SetTimeLock(a.expect("no time lock")))) + ("time/lock" [] Some(Self::SetTimeLock(!state.time_lock().get()))) +}); +atom_command!(PoolClipCommand: |state: MidiPool| { + ("add" [i: usize, c: MidiClip] + Some(Self::Add(i.expect("no index"), c.expect("no clip")))) + ("delete" [i: usize] + Some(Self::Delete(i.expect("no index")))) + ("swap" [a: usize, b: usize] + Some(Self::Swap(a.expect("no index"), b.expect("no index")))) + ("import" [i: usize, p: PathBuf] + Some(Self::Import(i.expect("no index"), p.expect("no path")))) + ("export" [i: usize, p: PathBuf] + Some(Self::Export(i.expect("no index"), p.expect("no path")))) + ("set-name" [i: usize, n: Arc] + Some(Self::SetName(i.expect("no index"), n.expect("no name")))) + ("set-length" [i: usize, l: usize] + Some(Self::SetLength(i.expect("no index"), l.expect("no length")))) + ("set-color" [i: usize, c: ItemColor] + Some(Self::SetColor(i.expect("no index"), c.expect("no color")))) +}); +// TODO: 1-9 seek markers that by default start every 8th of the clip +defcom!([self, state: MidiEditor] + (MidiEditCommand + (AppendNote [] { state.put_note(true); None }) + (PutNote [] { state.put_note(false); None }) + (DelNote [] { None }) + (SetNoteCursor [x: usize] { state.set_note_pos(x.min(127)); None }) + (SetNoteLength [x: usize] { + let note_len = state.note_len(); + let time_zoom = state.time_zoom().get(); + state.set_note_len(x); + //if note_len / time_zoom != x / time_zoom { + state.redraw(); + //} + None + }) + (SetNoteScroll [x: usize] { state.note_lo().set(x.min(127)); None }) + (SetTimeCursor [x: usize] { state.set_time_pos(x); None }) + (SetTimeScroll [x: usize] { state.time_start().set(x); None }) + (SetTimeZoom [x: usize] { state.time_zoom().set(x); state.redraw(); None }) + (SetTimeLock [x: bool] { state.time_lock().set(x); None }) + (Show [x: MaybeClip] { state.set_clip(x.as_ref()); None }))); + +#[derive(Clone, Debug, PartialEq)] pub enum ClipRenameCommand { + Begin, + Cancel, + Confirm, + Set(Arc), +} + + +command!(|self: ClipRenameCommand, state: MidiPool|if let Some( + PoolMode::Rename(clip, ref mut old_name) +) = state.mode_mut().clone() { + match self { + Self::Set(s) => { + state.clips()[clip].write().unwrap().name = s; + return Ok(Some(Self::Set(old_name.clone().into()))) + }, + Self::Confirm => { + let old_name = old_name.clone(); + *state.mode_mut() = None; + return Ok(Some(Self::Set(old_name))) + }, + Self::Cancel => { + state.clips()[clip].write().unwrap().name = old_name.clone().into(); + return Ok(None) + }, + _ => unreachable!() + } +} else { + unreachable!() +}); + +command!(|self: FileBrowserCommand, state: MidiPool|{ + use PoolMode::*; + use FileBrowserCommand::*; + let mode = &mut state.mode; + match mode { + Some(Import(index, ref mut browser)) => match self { + Cancel => { *mode = None; }, + Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); }, + Select(index) => { browser.index = index; }, + Confirm => if browser.is_file() { + let index = *index; + let path = browser.path(); + *mode = None; + PoolClipCommand::Import(index, path).execute(state)?; + } else if browser.is_dir() { + *mode = Some(Import(*index, browser.chdir()?)); + }, + _ => todo!(), + }, + Some(Export(index, ref mut browser)) => match self { + Cancel => { *mode = None; }, + Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); }, + Select(index) => { browser.index = index; }, + _ => unreachable!() + }, + _ => unreachable!(), + }; + None +}); + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ClipLengthCommand { + Begin, + Cancel, + Set(usize), + Next, + Prev, + Inc, + Dec, +} + +command!(|self: ClipLengthCommand, state: MidiPool|{ + use ClipLengthCommand::*; + use ClipLengthFocus::*; + if let Some( + PoolMode::Length(clip, ref mut length, ref mut focus) + ) = state.mode_mut().clone() { + match self { + Cancel => { *state.mode_mut() = None; }, + Prev => { focus.prev() }, + Next => { focus.next() }, + Inc => match focus { + Bar => { *length += 4 * PPQ }, + Beat => { *length += PPQ }, + Tick => { *length += 1 }, + }, + Dec => match focus { + Bar => { *length = length.saturating_sub(4 * PPQ) }, + Beat => { *length = length.saturating_sub(PPQ) }, + Tick => { *length = length.saturating_sub(1) }, + }, + Set(length) => { + let old_length; + { + let clip = state.clips()[clip].clone();//.write().unwrap(); + old_length = Some(clip.read().unwrap().length); + clip.write().unwrap().length = length; + } + *state.mode_mut() = None; + return Ok(old_length.map(Self::Set)) + }, + _ => unreachable!() + } + } else { + unreachable!(); + } + None +}); diff --git a/crates/app/src/config.rs b/crates/app/src/config.rs new file mode 100644 index 00000000..f876371b --- /dev/null +++ b/crates/app/src/config.rs @@ -0,0 +1,161 @@ +use crate::*; +use std::path::PathBuf; + +/// Configuration +#[derive(Default, Debug)] +pub struct Configuration { + /// Path of configuration entrypoint + pub path: PathBuf, + /// Name of configuration + pub name: Option>, + /// Description of configuration + pub info: Option>, + /// View definition + pub view: TokenIter<'static>, + // Input keymap + pub keys: InputMap<'static, Tek, TekCommand, TuiIn, TokenIter<'static>> +} + +impl Configuration { + + pub fn new (path: &impl AsRef, _watch: bool) -> Usually { + let text = read_and_leak(path.as_ref())?; + let [name, info, view, keys] = Self::parse(TokenIter::from(text))?; + Ok(Self { + path: path.as_ref().into(), + info: info.map(Self::parse_info).flatten(), + name: name.map(Self::parse_name).flatten(), + view: Self::parse_view(view)?, + keys: Self::parse_keys(&path, keys)?, + }) + } + + fn parse (iter: TokenIter) -> Usually<[Option;4]> { + let mut name: Option = None; + let mut info: Option = None; + let mut view: Option = None; + let mut keys: Option = None; + for token in iter { + match token.value { + Value::Exp(_, mut exp) => { + let next = exp.next(); + match next { + Some(Token { value: Value::Key(sym), .. }) => match sym { + "name" => name = Some(exp), + "info" => info = Some(exp), + "keys" => keys = Some(exp), + "view" => view = Some(exp), + _ => return Err( + format!("(e3) unexpected symbol {sym:?}").into() + ) + }, + _ => return Err( + format!("(e2) unexpected exp {:?}", next.map(|x|x.value)).into() + ) + } + }, + t => return Err( + format!("(e1) unexpected token {token:?}").into() + ) + }; + } + Ok([name, info, view, keys]) + } + + fn parse_info (mut iter: TokenIter) -> Option> { + iter.next().and_then(|x|if let Value::Str(x) = x.value { + Some(x.into()) + } else { + None + }) + } + + fn parse_name (mut iter: TokenIter) -> Option> { + iter.next().and_then(|x|if let Value::Str(x) = x.value { + Some(x.into()) + } else { + None + }) + } + + fn parse_view (iter: Option) -> Usually { + if let Some(view) = iter { + Ok(view) + } else { + Err(format!("missing view definition").into()) + } + } + + fn parse_keys (base: &impl AsRef, iter: Option) + -> Usually>> + { + if let Some(mut keys) = iter { + let mut map = InputMap::default(); + while let Some(token) = keys.next() { + match token.value { + Value::Exp(_, mut exp) => { + let next = exp.next(); + match next { + Some(Token { value: Value::Key(sym), .. }) => match sym { + "layer" => { + let next = exp.next(); + match next { + Some(Token { value: Value::Str(path), .. }) => { + map.add_layer(read_and_leak(path)?.into()); + }, + _ => return Err( + format!("(e4) unexpected non-string {next:?}").into() + ) + } + todo!() + }, + "layer-if" => { + let next = exp.next(); + match next { + Some(Token { value: Value::Sym(sym), .. }) => { + todo!() + }, + _ => return Err( + format!("(e4) unexpected non-symbol {next:?}").into() + ) + } + let next = exp.next(); + match next { + Some(Token { value: Value::Str(path), .. }) => { + todo!() + }, + _ => return Err( + format!("(e4) unexpected non-symbol {next:?}").into() + ) + } + todo!() + }, + _ => return Err( + format!("(e3) unexpected symbol {sym:?}").into() + ) + } + _ => return Err( + format!("(e2) unexpected exp {:?}", next.map(|x|x.value)).into() + ) + } + }, + t => return Err( + format!("(e1) unexpected token {token:?}").into() + ) + } + } + Ok(map) + } else { + return Err(format!("missing keys definition").into()) + } + } + +} + +fn read_and_leak (path: impl AsRef) -> Usually<&'static str> { + let path = path.as_ref(); + let text = String::from_utf8(std::fs::read(path)?)?; + let text: Box = text.into(); + let text: &'static str = Box::leak(text); + Ok(text) +} diff --git a/crates/app/src/editor.rs b/crates/app/src/editor.rs deleted file mode 100644 index 62c7b812..00000000 --- a/crates/app/src/editor.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod editor_api; -mod editor_model; -mod editor_view; diff --git a/crates/app/src/editor/editor_api.rs b/crates/app/src/editor/editor_api.rs deleted file mode 100644 index 1ac85118..00000000 --- a/crates/app/src/editor/editor_api.rs +++ /dev/null @@ -1,97 +0,0 @@ -use crate::*; - -provide!(bool: |self: MidiEditor| { - ":true" => true, - ":false" => false, - ":time-lock" => self.time_lock().get(), - ":time-lock-toggle" => !self.time_lock().get(), -}); - -provide!(usize: |self: MidiEditor| { - ":note-length" => self.note_len(), - - ":note-pos" => self.note_pos(), - ":note-pos-next" => self.note_pos() + 1, - ":note-pos-prev" => self.note_pos().saturating_sub(1), - ":note-pos-next-octave" => self.note_pos() + 12, - ":note-pos-prev-octave" => self.note_pos().saturating_sub(12), - - ":note-len" => self.note_len(), - ":note-len-next" => self.note_len() + 1, - ":note-len-prev" => self.note_len().saturating_sub(1), - - ":note-range" => self.note_axis().get(), - ":note-range-prev" => self.note_axis().get() + 1, - ":note-range-next" => self.note_axis().get().saturating_sub(1), - - ":time-pos" => self.time_pos(), - ":time-pos-next" => self.time_pos() + self.time_zoom().get(), - ":time-pos-prev" => self.time_pos().saturating_sub(self.time_zoom().get()), - - ":time-zoom" => self.time_zoom().get(), - ":time-zoom-next" => self.time_zoom().get() + 1, - ":time-zoom-prev" => self.time_zoom().get().saturating_sub(1).max(1), -}); - -atom_command!(MidiEditCommand: |state: MidiEditor| { - ("note/append" [] Some(Self::AppendNote)) - ("note/put" [] Some(Self::PutNote)) - ("note/del" [] Some(Self::DelNote)) - ("note/pos" [a: usize] Some(Self::SetNoteCursor(a.expect("no note cursor")))) - ("note/len" [a: usize] Some(Self::SetNoteLength(a.expect("no note length")))) - ("time/pos" [a: usize] Some(Self::SetTimeCursor(a.expect("no time cursor")))) - ("time/zoom" [a: usize] Some(Self::SetTimeZoom(a.expect("no time zoom")))) - ("time/lock" [a: bool] Some(Self::SetTimeLock(a.expect("no time lock")))) - ("time/lock" [] Some(Self::SetTimeLock(!state.time_lock().get()))) -}); - -#[derive(Clone, Debug)] pub enum MidiEditCommand { - // TODO: 1-9 seek markers that by default start every 8th of the clip - AppendNote, - PutNote, - DelNote, - SetNoteCursor(usize), - SetNoteLength(usize), - SetNoteScroll(usize), - SetTimeCursor(usize), - SetTimeScroll(usize), - SetTimeZoom(usize), - SetTimeLock(bool), - Show(Option>>), -} - -handle!(TuiIn: |self: MidiEditor, input|Ok(if let Some(command) = self.keys.command(self, input) { - command.execute(self)?; - Some(true) -} else { - None -})); - -impl Command for MidiEditCommand { - fn execute (self, state: &mut MidiEditor) -> Perhaps { - use MidiEditCommand::*; - match self { - Show(clip) => { state.set_clip(clip.as_ref()); }, - DelNote => {}, - PutNote => { state.put_note(false); }, - AppendNote => { state.put_note(true); }, - SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); }, - SetTimeLock(x) => { state.time_lock().set(x); }, - SetTimeScroll(x) => { state.time_start().set(x); }, - SetNoteScroll(x) => { state.note_lo().set(x.min(127)); }, - SetNoteLength(x) => { - let note_len = state.note_len(); - let time_zoom = state.time_zoom().get(); - state.set_note_len(x); - //if note_len / time_zoom != x / time_zoom { - state.redraw(); - //} - }, - SetTimeCursor(x) => { state.set_time_pos(x); }, - SetNoteCursor(note) => { state.set_note_pos(note.min(127)); }, - //_ => todo!("{:?}", self) - } - Ok(None) - } -} - diff --git a/crates/app/src/editor/editor_model.rs b/crates/app/src/editor/editor_model.rs deleted file mode 100644 index 29b7ba25..00000000 --- a/crates/app/src/editor/editor_model.rs +++ /dev/null @@ -1,179 +0,0 @@ -//! MIDI editor. -use crate::*; - -/// Contains state for viewing and editing a clip -pub struct MidiEditor { - /// Size of editor on screen - pub size: Measure, - /// View mode and state of editor - pub mode: PianoHorizontal, - /// Input keymap - pub keys: InputMap<'static, Self, MidiEditCommand, TuiIn, SourceIter<'static>> -} - -impl std::fmt::Debug for MidiEditor { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("MidiEditor") - .field("mode", &self.mode) - .finish() - } -} - -impl Default for MidiEditor { - fn default () -> Self { - Self { - size: Measure::new(), - mode: PianoHorizontal::new(None), - keys: InputMap::new(SourceIter(include_str!("../../../../config/keys_editor.edn"))), - } - } -} - - -has_size!(|self: MidiEditor|&self.size); - -content!(TuiOut: |self: MidiEditor| { - self.autoscroll(); - //self.autozoom(); - self.size.of(&self.mode) -}); - -from!(|clip: &Arc>|MidiEditor = { - let model = Self::from(Some(clip.clone())); - model.redraw(); - model -}); - -from!(|clip: Option>>|MidiEditor = { - let mut model = Self::default(); - *model.clip_mut() = clip; - model.redraw(); - model -}); - -impl MidiEditor { - - /// Put note at current position - pub fn put_note (&mut self, advance: bool) { - let mut redraw = false; - if let Some(clip) = self.clip() { - let mut clip = clip.write().unwrap(); - let note_start = self.time_pos(); - let note_pos = self.note_pos(); - let note_len = self.note_len(); - let note_end = note_start + (note_len.saturating_sub(1)); - let key: u7 = u7::from(note_pos as u8); - let vel: u7 = 100.into(); - let length = clip.length; - let note_end = note_end % length; - let note_on = MidiMessage::NoteOn { key, vel }; - if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { - clip.notes[note_start].push(note_on); - } - let note_off = MidiMessage::NoteOff { key, vel }; - if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { - clip.notes[note_end].push(note_off); - } - if advance { - self.set_time_pos(note_end); - } - redraw = true; - } - if redraw { - self.mode.redraw(); - } - } - - pub fn clip_status (&self) -> impl Content + '_ { - let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.name.clone(), clip.length, clip.looped) - } else { (ItemTheme::G[64], String::new().into(), 0, false) }; - Bsp::e( - FieldH(color, "Edit", format!("{name} ({length})")), - FieldH(color, "Loop", looped.to_string()) - ) - } - - pub fn edit_status (&self) -> impl Content + '_ { - let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.length) - } else { (ItemTheme::G[64], 0) }; - let time_pos = self.time_pos(); - let time_zoom = self.time_zoom().get(); - let time_lock = if self.time_lock().get() { "[lock]" } else { " " }; - let note_pos = format!("{:>3}", self.note_pos()); - let note_name = format!("{:4}", Note::pitch_to_name(self.note_pos())); - let note_len = format!("{:>4}", self.note_len()); - Bsp::e( - FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos} {time_lock}")), - FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")), - ) - } - - //fn clip_length (&self) -> usize { - //self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) - //} -} - -impl TimeRange for MidiEditor { - fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } -} - -impl NoteRange for MidiEditor { - fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } -} - -impl NotePoint for MidiEditor { - fn note_len (&self) -> usize { self.mode.note_len() } - fn set_note_len (&self, x: usize) -> usize { self.mode.set_note_len(x) } - fn note_pos (&self) -> usize { self.mode.note_pos() } - fn set_note_pos (&self, x: usize) -> usize { self.mode.set_note_pos(x) } -} - -impl TimePoint for MidiEditor { - fn time_pos (&self) -> usize { self.mode.time_pos() } - fn set_time_pos (&self, x: usize) -> usize { self.mode.set_time_pos(x) } -} - -impl MidiViewer for MidiEditor { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } - fn redraw (&self) { self.mode.redraw() } - fn clip (&self) -> &Option>> { self.mode.clip() } - fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } - fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } -} - -pub trait HasEditor { - fn editor (&self) -> &Option; - fn editor_mut (&mut self) -> &Option; - fn is_editing (&self) -> bool { true } - fn editor_w (&self) -> usize { 0 } - fn editor_h (&self) -> usize { 0 } -} - -#[macro_export] macro_rules! has_editor { - (|$self:ident: $Struct:ident|{ - editor = $e0:expr; - editor_w = $e1:expr; - editor_h = $e2:expr; - is_editing = $e3:expr; - }) => { - impl HasEditor for $Struct { - fn editor (&$self) -> &Option { &$e0 } - fn editor_mut (&mut $self) -> &Option { &mut $e0 } - fn editor_w (&$self) -> usize { $e1 } - fn editor_h (&$self) -> usize { $e2 } - fn is_editing (&$self) -> bool { $e3 } - } - }; - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? { - fn editor (&$self) -> &MidiEditor { &$cb } - } - }; -} diff --git a/crates/app/src/editor/editor_view.rs b/crates/app/src/editor/editor_view.rs deleted file mode 100644 index 67c9dc18..00000000 --- a/crates/app/src/editor/editor_view.rs +++ /dev/null @@ -1,363 +0,0 @@ -use crate::*; -use Color::*; - -/// A clip, rendered as a horizontal piano roll. -#[derive(Clone)] -pub struct PianoHorizontal { - pub clip: Option>>, - /// Buffer where the whole clip is rerendered on change - pub buffer: Arc>, - /// Size of actual notes area - pub size: Measure, - /// The display window - pub range: MidiRangeModel, - /// The note cursor - pub point: MidiPointModel, - /// The highlight color palette - pub color: ItemTheme, - /// Width of the keyboard - pub keys_width: u16, -} - -impl PianoHorizontal { - pub fn new (clip: Option<&Arc>>) -> Self { - let size = Measure::new(); - let mut range = MidiRangeModel::from((12, true)); - range.time_axis = size.x.clone(); - range.note_axis = size.y.clone(); - let piano = Self { - keys_width: 5, - size, - range, - buffer: RwLock::new(Default::default()).into(), - point: MidiPointModel::default(), - clip: clip.cloned(), - color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), - }; - piano.redraw(); - piano - } -} - -pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator { - (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) -} - -content!(TuiOut:|self: PianoHorizontal| Tui::bg(Tui::g(40), Bsp::s( - Bsp::e( - Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())), - self.timeline() - ), - Bsp::e( - self.keys(), - self.size.of(Tui::bg(Tui::g(32), Bsp::b( - Fill::xy(self.notes()), - Fill::xy(self.cursor()), - ))) - ), -))); - -impl PianoHorizontal { - /// Draw the piano roll background. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize) { - for (y, note) in (0..=127).rev().enumerate() { - for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { - let cell = buf.get_mut(x, y).unwrap(); - cell.set_bg(clip.color.darkest.rgb); - if time % 384 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('│'); - } else if time % 96 == 0 { - cell.set_fg(clip.color.dark.rgb); - cell.set_char('╎'); - } else if time % note_len == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('┊'); - } else if (127 - note) % 12 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('='); - } else if (127 - note) % 6 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('—'); - } else { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('·'); - } - } - } - } - /// Draw the piano roll foreground. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { - let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); - let mut notes_on = [false;128]; - for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { - for (_y, note) in (0..=127).rev().enumerate() { - if let Some(cell) = buf.get_mut(x, note) { - if notes_on[note] { - cell.set_char('▂'); - cell.set_style(style); - } - } - } - let time_end = time_start + zoom; - for time in time_start..time_end.min(clip.length) { - for event in clip.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - if let Some(cell) = buf.get_mut(x, note) { - cell.set_char('█'); - cell.set_style(style); - } - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } - } - } - - } - } - fn notes (&self) -> impl Content { - let time_start = self.time_start().get(); - let note_lo = self.note_lo().get(); - let note_hi = self.note_hi(); - let buffer = self.buffer.clone(); - ThunkRender::new(move|to: &mut TuiOut|{ - let source = buffer.read().unwrap(); - let [x0, y0, w, _h] = to.area().xywh(); - //if h as usize != note_axis { - //panic!("area height mismatch: {h} <> {note_axis}"); - //} - for (area_x, screen_x) in (x0..x0+w).enumerate() { - for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { - let source_x = time_start + area_x; - let source_y = note_hi - area_y; - // TODO: enable loop rollover: - //let source_x = (time_start + area_x) % source.width.max(1); - //let source_y = (note_hi - area_y) % source.height.max(1); - let is_in_x = source_x < source.width; - let is_in_y = source_y < source.height; - if is_in_x && is_in_y { - if let Some(source_cell) = source.get(source_x, source_y) { - if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { - *cell = source_cell.clone(); - } - } - } - } - } - }) - } - fn cursor (&self) -> impl Content { - let style = Some(Style::default().fg(self.color.lightest.rgb)); - let note_hi = self.note_hi(); - let note_lo = self.note_lo().get(); - let note_pos = self.note_pos(); - let note_len = self.note_len(); - let time_pos = self.time_pos(); - let time_start = self.time_start().get(); - let time_zoom = self.time_zoom().get(); - ThunkRender::new(move|to: &mut TuiOut|{ - let [x0, y0, w, _] = to.area().xywh(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - if note == note_pos { - for x in 0..w { - let screen_x = x0 + x; - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_pos && time_pos < time_2 { - to.blit(&"█", screen_x, screen_y, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (screen_x + 1)..(screen_x + tail) { - to.blit(&"▂", x_tail, screen_y, style); - } - break - } - } - break - } - } - }) - } - fn keys (&self) -> impl Content { - let state = self; - let color = state.color; - let note_lo = state.note_lo().get(); - let note_hi = state.note_hi(); - let note_pos = state.note_pos(); - let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); - let off_style = Some(Style::default().fg(Tui::g(255))); - let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); - Fill::y(Fixed::x(self.keys_width, ThunkRender::new(move|to: &mut TuiOut|{ - let [x, y0, _w, _h] = to.area().xywh(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - to.blit(&to_key(note), x, screen_y, key_style); - if note > 127 { - continue - } - if note == note_pos { - to.blit(&format!("{:<5}", Note::pitch_to_name(note)), x, screen_y, on_style) - } else { - to.blit(&Note::pitch_to_name(note), x, screen_y, off_style) - }; - } - }))) - } - fn timeline (&self) -> impl Content + '_ { - Fill::x(Fixed::y(1, ThunkRender::new(move|to: &mut TuiOut|{ - let [x, y, w, _h] = to.area(); - let style = Some(Style::default().dim()); - let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { - let t = area_x as usize * self.time_zoom().get(); - if t < length { - to.blit(&"|", screen_x, y, style); - } - } - }))) - } -} - -has_size!(|self:PianoHorizontal|&self.size); - -impl TimeRange for PianoHorizontal { - fn time_len (&self) -> &AtomicUsize { self.range.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.range.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } -} - -impl NoteRange for PianoHorizontal { - fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } -} - -impl NotePoint for PianoHorizontal { - fn note_len (&self) -> usize { self.point.note_len() } - fn set_note_len (&self, x: usize) -> usize { self.point.set_note_len(x) } - fn note_pos (&self) -> usize { self.point.note_pos() } - fn set_note_pos (&self, x: usize) -> usize { self.point.set_note_pos(x) } -} - -impl TimePoint for PianoHorizontal { - fn time_pos (&self) -> usize { self.point.time_pos() } - fn set_time_pos (&self, x: usize) -> usize { self.point.set_time_pos(x) } -} - -impl MidiViewer for PianoHorizontal { - fn clip (&self) -> &Option>> { - &self.clip - } - fn clip_mut (&mut self) -> &mut Option>> { - &mut self.clip - } - /// Determine the required space to render the clip. - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { - (clip.length / self.range.time_zoom().get(), 128) - } - fn redraw (&self) { - *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { - let clip = clip.read().unwrap(); - let buf_size = self.buffer_size(&clip); - let mut buffer = BigBuffer::from(buf_size); - let note_len = self.note_len(); - let time_zoom = self.time_zoom().get(); - self.time_len().set(clip.length); - PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom, note_len); - PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); - buffer - } else { - Default::default() - } - } - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.color = clip.map(|p|p.read().unwrap().color) - .unwrap_or(ItemTheme::G[64]); - self.redraw(); - } -} - -impl std::fmt::Debug for PianoHorizontal { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - let buffer = self.buffer.read().unwrap(); - f.debug_struct("PianoHorizontal") - .field("time_zoom", &self.range.time_zoom) - .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) - .finish() - } -} - // Update sequencer playhead indicator - //self.now().set(0.); - //if let Some((ref started_at, Some(ref playing))) = self.player.play_clip { - //let clip = clip.read().unwrap(); - //if *playing.read().unwrap() == *clip { - //let pulse = self.current().pulse.get(); - //let start = started_at.pulse.get(); - //let now = (pulse - start) % clip.length as f64; - //self.now().set(now); - //} - //} - -fn to_key (note: usize) -> &'static str { - match note % 12 { - 11 => "████▌", - 10 => " ", - 9 => "████▌", - 8 => " ", - 7 => "████▌", - 6 => " ", - 5 => "████▌", - 4 => "████▌", - 3 => " ", - 2 => "████▌", - 1 => " ", - 0 => "████▌", - _ => unreachable!(), - } -} - -pub struct OctaveVertical { - on: [bool; 12], - colors: [Color; 3] -} - -impl Default for OctaveVertical { - fn default () -> Self { - Self { - on: [false; 12], - colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] - } - } -} - -impl OctaveVertical { - fn color (&self, pitch: usize) -> Color { - let pitch = pitch % 12; - self.colors[if self.on[pitch] { 2 } else { - match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } - }] - } -} - -impl Content for OctaveVertical { - fn content (&self) -> impl Render { - row!( - Tui::fg_bg(self.color(0), self.color(1), "▙"), - Tui::fg_bg(self.color(2), self.color(3), "▙"), - Tui::fg_bg(self.color(4), self.color(5), "▌"), - Tui::fg_bg(self.color(6), self.color(7), "▟"), - Tui::fg_bg(self.color(8), self.color(9), "▟"), - Tui::fg_bg(self.color(10), self.color(11), "▟"), - ) - } -} diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index 3e438bae..ef4e3a64 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -31,16 +31,16 @@ pub use ::tengri::tui::ratatui::prelude::Color::{self, *}; pub use ::tengri::tui::ratatui::prelude::{Style, Stylize, Buffer, Modifier}; pub use ::tengri::tui::crossterm; pub use ::tengri::tui::crossterm::event::{Event, KeyCode::{self, *}}; -pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::Relaxed}}; +pub(crate) use std::path::Path; +pub(crate) use std::sync::{Arc, RwLock}; +pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}; mod api; pub use self::api::*; mod audio; pub use self::audio::*; +mod config; pub use self::config::*; mod model; pub use self::model::*; mod view; pub use self::view::*; -mod pool; -mod editor; - #[cfg(test)] #[test] fn test_model () { let mut tek = Tek::default(); let _ = tek.clip(); diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index ab1e0fb9..b8e6471c 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -48,14 +48,12 @@ pub struct Tek { pub history: Vec, /// Port handles pub ports: std::collections::BTreeMap>, - /// View definition - pub view: SourceIter<'static>, // Cache of formatted strings pub view_cache: Arc>, // Modal overlay pub modal: Option, - // Input keymap - pub keys: InputMap<'static, Self, TekCommand, TuiIn, TokenIter<'static>> + // View and input definition + pub config: Configuration } impl Tek { @@ -442,11 +440,11 @@ pub enum Selection { /// A track is selected. Track(usize), /// A clip (track × scene) is selected. - TrackClip { track: usize, scene: usize }, + TrackClip { track: usize, scene: usize }, /// A track's MIDI input connection is selected. - TrackInput { track: usize, port: usize }, + TrackInput { track: usize, port: usize }, /// A track's MIDI output connection is selected. - TrackOutput { track: usize, port: usize }, + TrackOutput { track: usize, port: usize }, /// A track device slot is selected. TrackDevice { track: usize, device: usize }, } @@ -516,7 +514,7 @@ impl Selection { pub fn scene_next (&self, len: usize) -> Self { use Selection::*; match self { - Mix => Scene(0), + Mix => Scene(0), Track(t) => TrackClip { track: *t, scene: 0 }, Scene(s) => if s + 1 < len { Scene(s + 1) @@ -546,9 +544,11 @@ impl Selection { use Selection::*; format!("{}", match self { Mix => "Everything".to_string(), - Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)) + Scene(s) => scenes.get(*s) + .map(|scene|format!("S{s}: {}", &scene.name)) .unwrap_or_else(||"S??".into()), - Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)) + Track(t) => tracks.get(*t) + .map(|track|format!("T{t}: {}", &track.name)) .unwrap_or_else(||"T??".into()), TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { (Some(_), Some(s)) => match s.clip(*track) { @@ -787,3 +787,374 @@ impl HasTracks for Tek { fn tracks (&self) -> &Vec { &self.tracks } fn tracks_mut (&mut self) -> &mut Vec { &mut self.tracks } } + +#[derive(Debug)] +pub struct MidiPool { + pub visible: bool, + /// Collection of clips + pub clips: Arc>>>>, + /// Selected clip + pub clip: AtomicUsize, + /// Mode switch + pub mode: Option, +} + +impl Default for MidiPool { + fn default () -> Self { + use PoolMode::*; + Self { + visible: true, + clips: Arc::from(RwLock::from(vec![])), + clip: 0.into(), + mode: None, + } + } +} + +has_clips!(|self: MidiPool|self.clips); + +has_clip!(|self: MidiPool|self.clips().get(self.clip_index()).map(|c|c.clone())); + +from!(|clip:&Arc>|MidiPool = { + let model = Self::default(); + model.clips.write().unwrap().push(clip.clone()); + model.clip.store(1, Relaxed); + model +}); + +impl MidiPool { + pub fn clip_index (&self) -> usize { + self.clip.load(Relaxed) + } + pub fn set_clip_index (&self, value: usize) { + self.clip.store(value, Relaxed); + } + pub fn mode (&self) -> &Option { + &self.mode + } + pub fn mode_mut (&mut self) -> &mut Option { + &mut self.mode + } + pub fn begin_clip_length (&mut self) { + let length = self.clips()[self.clip_index()].read().unwrap().length; + *self.mode_mut() = Some(PoolMode::Length( + self.clip_index(), + length, + ClipLengthFocus::Bar + )); + } + pub fn begin_clip_rename (&mut self) { + let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); + *self.mode_mut() = Some(PoolMode::Rename( + self.clip_index(), + name + )); + } + pub fn begin_import (&mut self) -> Usually<()> { + *self.mode_mut() = Some(PoolMode::Import( + self.clip_index(), + FileBrowser::new(None)? + )); + Ok(()) + } + pub fn begin_export (&mut self) -> Usually<()> { + *self.mode_mut() = Some(PoolMode::Export( + self.clip_index(), + FileBrowser::new(None)? + )); + Ok(()) + } + pub fn new_clip (&self) -> MidiClip { + MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) + } + pub fn cloned_clip (&self) -> MidiClip { + let index = self.clip_index(); + let mut clip = self.clips()[index].read().unwrap().duplicate(); + clip.color = ItemTheme::random_near(clip.color, 0.25); + clip + } + pub fn add_new_clip (&self) -> (usize, Arc>) { + let clip = Arc::new(RwLock::new(self.new_clip())); + let index = { + let mut clips = self.clips.write().unwrap(); + clips.push(clip.clone()); + clips.len().saturating_sub(1) + }; + self.clip.store(index, Relaxed); + (index, clip) + } + pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { + let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); + if let Some(index) = index { + self.clips.write().unwrap().remove(index); + return true + } + false + } +} + +/// Modes for clip pool +#[derive(Debug, Clone)] +pub enum PoolMode { + /// Renaming a pattern + Rename(usize, Arc), + /// Editing the length of a pattern + Length(usize, usize, ClipLengthFocus), + /// Load clip from disk + Import(usize, FileBrowser), + /// Save clip to disk + Export(usize, FileBrowser), +} + +/// Focused field of `ClipLength` +#[derive(Copy, Clone, Debug)] +pub enum ClipLengthFocus { + /// Editing the number of bars + Bar, + /// Editing the number of beats + Beat, + /// Editing the number of ticks + Tick, +} + +impl ClipLengthFocus { + pub fn next (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } + } + pub fn prev (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } + } +} + +/// Displays and edits clip length. +#[derive(Clone)] +pub struct ClipLength { + /// Pulses per beat (quaver) + ppq: usize, + /// Beats per bar + bpb: usize, + /// Length of clip in pulses + pulses: usize, + /// Selected subdivision + pub focus: Option, +} + +impl ClipLength { + pub fn _new (pulses: usize, focus: Option) -> Self { + Self { ppq: PPQ, bpb: 4, pulses, focus } + } + pub fn bars (&self) -> usize { + self.pulses / (self.bpb * self.ppq) + } + pub fn beats (&self) -> usize { + (self.pulses % (self.bpb * self.ppq)) / self.ppq + } + pub fn ticks (&self) -> usize { + self.pulses % self.ppq + } + pub fn bars_string (&self) -> Arc { + format!("{}", self.bars()).into() + } + pub fn beats_string (&self) -> Arc { + format!("{}", self.beats()).into() + } + pub fn ticks_string (&self) -> Arc { + format!("{:>02}", self.ticks()).into() + } +} + +pub type ClipPool = Vec>>; + +pub trait HasClips { + fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; + fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>; + fn add_clip (&self) -> (usize, Arc>) { + let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None))); + self.clips_mut().push(clip.clone()); + (self.clips().len() - 1, clip) + } +} + +#[macro_export] macro_rules! has_clips { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { + fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { + $cb.read().unwrap() + } + fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { + $cb.write().unwrap() + } + } + } +} + +pub trait HasEditor { + fn editor (&self) -> &Option; + fn editor_mut (&mut self) -> &Option; + fn is_editing (&self) -> bool { true } + fn editor_w (&self) -> usize { 0 } + fn editor_h (&self) -> usize { 0 } +} + +#[macro_export] macro_rules! has_editor { + (|$self:ident: $Struct:ident|{ + editor = $e0:expr; + editor_w = $e1:expr; + editor_h = $e2:expr; + is_editing = $e3:expr; + }) => { + impl HasEditor for $Struct { + fn editor (&$self) -> &Option { &$e0 } + fn editor_mut (&mut $self) -> &Option { &mut $e0 } + fn editor_w (&$self) -> usize { $e1 } + fn editor_h (&$self) -> usize { $e2 } + fn is_editing (&$self) -> bool { $e3 } + } + }; + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? { + fn editor (&$self) -> &MidiEditor { &$cb } + } + }; +} + +/// Contains state for viewing and editing a clip +pub struct MidiEditor { + /// Size of editor on screen + pub size: Measure, + /// View mode and state of editor + pub mode: PianoHorizontal, +} + +impl std::fmt::Debug for MidiEditor { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("MidiEditor") + .field("mode", &self.mode) + .finish() + } +} + +impl Default for MidiEditor { + fn default () -> Self { + Self { + size: Measure::new(), + mode: PianoHorizontal::new(None), + } + } +} + + +has_size!(|self: MidiEditor|&self.size); + +content!(TuiOut: |self: MidiEditor| { + self.autoscroll(); + //self.autozoom(); + self.size.of(&self.mode) +}); + +from!(|clip: &Arc>|MidiEditor = { + let model = Self::from(Some(clip.clone())); + model.redraw(); + model +}); + +from!(|clip: Option>>|MidiEditor = { + let mut model = Self::default(); + *model.clip_mut() = clip; + model.redraw(); + model +}); + +impl MidiEditor { + /// Put note at current position + pub fn put_note (&mut self, advance: bool) { + let mut redraw = false; + if let Some(clip) = self.clip() { + let mut clip = clip.write().unwrap(); + let note_start = self.time_pos(); + let note_pos = self.note_pos(); + let note_len = self.note_len(); + let note_end = note_start + (note_len.saturating_sub(1)); + let key: u7 = u7::from(note_pos as u8); + let vel: u7 = 100.into(); + let length = clip.length; + let note_end = note_end % length; + let note_on = MidiMessage::NoteOn { key, vel }; + if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { + clip.notes[note_start].push(note_on); + } + let note_off = MidiMessage::NoteOff { key, vel }; + if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { + clip.notes[note_end].push(note_off); + } + if advance { + self.set_time_pos(note_end); + } + redraw = true; + } + if redraw { + self.mode.redraw(); + } + } + + pub fn clip_status (&self) -> impl Content + '_ { + let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.name.clone(), clip.length, clip.looped) + } else { (ItemTheme::G[64], String::new().into(), 0, false) }; + Bsp::e( + FieldH(color, "Edit", format!("{name} ({length})")), + FieldH(color, "Loop", looped.to_string()) + ) + } + + pub fn edit_status (&self) -> impl Content + '_ { + let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.length) + } else { (ItemTheme::G[64], 0) }; + let time_pos = self.time_pos(); + let time_zoom = self.time_zoom().get(); + let time_lock = if self.time_lock().get() { "[lock]" } else { " " }; + let note_pos = format!("{:>3}", self.note_pos()); + let note_name = format!("{:4}", Note::pitch_to_name(self.note_pos())); + let note_len = format!("{:>4}", self.note_len()); + Bsp::e( + FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos} {time_lock}")), + FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")), + ) + } +} + +impl TimeRange for MidiEditor { + fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } +} + +impl NoteRange for MidiEditor { + fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } +} + +impl NotePoint for MidiEditor { + fn note_len (&self) -> usize { self.mode.note_len() } + fn set_note_len (&self, x: usize) -> usize { self.mode.set_note_len(x) } + fn note_pos (&self) -> usize { self.mode.note_pos() } + fn set_note_pos (&self, x: usize) -> usize { self.mode.set_note_pos(x) } +} + +impl TimePoint for MidiEditor { + fn time_pos (&self) -> usize { self.mode.time_pos() } + fn set_time_pos (&self, x: usize) -> usize { self.mode.set_time_pos(x) } +} + +impl MidiViewer for MidiEditor { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } + fn redraw (&self) { self.mode.redraw() } + fn clip (&self) -> &Option>> { self.mode.clip() } + fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } + fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } +} diff --git a/crates/app/src/pool.rs b/crates/app/src/pool.rs deleted file mode 100644 index a9964d05..00000000 --- a/crates/app/src/pool.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod pool_api; pub use self::pool_api::*; -mod pool_clips; pub use self::pool_clips::*; -mod pool_model; pub use self::pool_model::*; -mod pool_view; pub use self::pool_view::*; diff --git a/crates/app/src/pool/pool_api.rs b/crates/app/src/pool/pool_api.rs deleted file mode 100644 index 1a527e47..00000000 --- a/crates/app/src/pool/pool_api.rs +++ /dev/null @@ -1,314 +0,0 @@ -use crate::*; -use super::*; - -handle!(TuiIn: |self: MidiPool, input|{ - //Ok(if let Some(command) = match self.mode() { - //Some(PoolMode::Rename(..)) => self.keys_rename, - //Some(PoolMode::Length(..)) => self.keys_length, - //Some(PoolMode::Import(..)) | Some(PoolMode::Export(..)) => self.keys_file, - //_ => self.keys - //}.command::(self, input) { - Ok(if let Some(command) = self.keys.command(self, input) { - let _undo = command.execute(self)?; - Some(true) - } else { - None - }) -}); - -provide!(bool: |self: MidiPool| {}); - -provide!(MidiClip: |self: MidiPool| { - ":new-clip" => self.new_clip(), - ":cloned-clip" => self.cloned_clip(), -}); - -provide!(PathBuf: |self: MidiPool| {}); - -provide!(Arc: |self: MidiPool| {}); - -provide!(usize: |self: MidiPool| { - ":current" => 0, - ":after" => 0, - ":previous" => 0, - ":next" => 0 -}); - -provide!(ItemColor: |self: MidiPool| { - ":random-color" => ItemColor::random() -}); - -#[derive(Clone, PartialEq, Debug)] pub enum PoolCommand { - /// Toggle visibility of pool - Show(bool), - /// Select a clip from the clip pool - Select(usize), - /// Rename a clip - Rename(ClipRenameCommand), - /// Change the length of a clip - Length(ClipLengthCommand), - /// Import from file - Import(FileBrowserCommand), - /// Export to file - Export(FileBrowserCommand), - /// Update the contents of the clip pool - Clip(PoolClipCommand), -} - -atom_command!(PoolCommand: |state: MidiPool| { - ("show" [a: bool] Some(Self::Show(a.expect("no flag")))) - ("select" [i: usize] Some(Self::Select(i.expect("no index")))) - ("rename" [,..a] ClipRenameCommand::try_from_expr(state, a).map(Self::Rename)) - ("length" [,..a] ClipLengthCommand::try_from_expr(state, a).map(Self::Length)) - ("import" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Import)) - ("export" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Export)) - ("clip" [,..a] PoolClipCommand::try_from_expr(state, a).map(Self::Clip)) -}); - -command!(|self: PoolCommand, state: MidiPool|{ - use PoolCommand::*; - match self { - Rename(ClipRenameCommand::Begin) => { state.begin_clip_rename(); None } - Rename(command) => command.delegate(state, Rename)?, - Length(ClipLengthCommand::Begin) => { state.begin_clip_length(); None }, - Length(command) => command.delegate(state, Length)?, - Import(FileBrowserCommand::Begin) => { state.begin_import()?; None }, - Import(command) => command.delegate(state, Import)?, - Export(FileBrowserCommand::Begin) => { state.begin_export()?; None }, - Export(command) => command.delegate(state, Export)?, - Clip(command) => command.execute(state)?.map(Clip), - Show(visible) => { state.visible = visible; Some(Self::Show(!visible)) }, - Select(clip) => { state.set_clip_index(clip); None }, - } -}); - -#[derive(Clone, Debug, PartialEq)] pub enum PoolClipCommand { - Add(usize, MidiClip), - Delete(usize), - Swap(usize, usize), - Import(usize, PathBuf), - Export(usize, PathBuf), - SetName(usize, Arc), - SetLength(usize, usize), - SetColor(usize, ItemColor), -} - -atom_command!(PoolClipCommand: |state: MidiPool| { - ("add" [i: usize, c: MidiClip] - Some(Self::Add(i.expect("no index"), c.expect("no clip")))) - ("delete" [i: usize] - Some(Self::Delete(i.expect("no index")))) - ("swap" [a: usize, b: usize] - Some(Self::Swap(a.expect("no index"), b.expect("no index")))) - ("import" [i: usize, p: PathBuf] - Some(Self::Import(i.expect("no index"), p.expect("no path")))) - ("export" [i: usize, p: PathBuf] - Some(Self::Export(i.expect("no index"), p.expect("no path")))) - ("set-name" [i: usize, n: Arc] - Some(Self::SetName(i.expect("no index"), n.expect("no name")))) - ("set-length" [i: usize, l: usize] - Some(Self::SetLength(i.expect("no index"), l.expect("no length")))) - ("set-color" [i: usize, c: ItemColor] - Some(Self::SetColor(i.expect("no index"), c.expect("no color")))) -}); - -impl Command for PoolClipCommand { - fn execute (self, model: &mut T) -> Perhaps { - use PoolClipCommand::*; - Ok(match self { - Add(mut index, clip) => { - let clip = Arc::new(RwLock::new(clip)); - let mut clips = model.clips_mut(); - if index >= clips.len() { - index = clips.len(); - clips.push(clip) - } else { - clips.insert(index, clip); - } - Some(Self::Delete(index)) - }, - Delete(index) => { - let clip = model.clips_mut().remove(index).read().unwrap().clone(); - Some(Self::Add(index, clip)) - }, - Swap(index, other) => { - model.clips_mut().swap(index, other); - Some(Self::Swap(index, other)) - }, - Import(index, path) => { - let bytes = std::fs::read(&path)?; - let smf = Smf::parse(bytes.as_slice())?; - let mut t = 0u32; - let mut events = vec![]; - for track in smf.tracks.iter() { - for event in track.iter() { - t += event.delta.as_int(); - if let TrackEventKind::Midi { channel, message } = event.kind { - events.push((t, channel.as_int(), message)); - } - } - } - let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None); - for event in events.iter() { - clip.notes[event.0 as usize].push(event.2); - } - Self::Add(index, clip).execute(model)? - }, - Export(_index, _path) => { - todo!("export clip to midi file"); - }, - SetName(index, name) => { - let clip = &mut model.clips_mut()[index]; - let old_name = clip.read().unwrap().name.clone(); - clip.write().unwrap().name = name; - Some(Self::SetName(index, old_name)) - }, - SetLength(index, length) => { - let clip = &mut model.clips_mut()[index]; - let old_len = clip.read().unwrap().length; - clip.write().unwrap().length = length; - Some(Self::SetLength(index, old_len)) - }, - SetColor(index, color) => { - let mut color = ItemTheme::from(color); - std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color); - Some(Self::SetColor(index, color.base)) - }, - }) - } -} - -#[derive(Clone, Debug, PartialEq)] pub enum ClipRenameCommand { - Begin, - Cancel, - Confirm, - Set(Arc), -} - -atom_command!(ClipRenameCommand: |state: MidiPool| { - ("begin" [] Some(Self::Begin)) - ("cancel" [] Some(Self::Cancel)) - ("confirm" [] Some(Self::Confirm)) - ("set" [n: Arc] Some(Self::Set(n.expect("no name")))) -}); - -command!(|self: ClipRenameCommand, state: MidiPool|if let Some( - PoolMode::Rename(clip, ref mut old_name) -) = state.mode_mut().clone() { - match self { - Self::Set(s) => { - state.clips()[clip].write().unwrap().name = s; - return Ok(Some(Self::Set(old_name.clone().into()))) - }, - Self::Confirm => { - let old_name = old_name.clone(); - *state.mode_mut() = None; - return Ok(Some(Self::Set(old_name))) - }, - Self::Cancel => { - state.clips()[clip].write().unwrap().name = old_name.clone().into(); - return Ok(None) - }, - _ => unreachable!() - } -} else { - unreachable!() -}); - -atom_command!(FileBrowserCommand: |state: MidiPool| { - ("begin" [] Some(Self::Begin)) - ("cancel" [] Some(Self::Cancel)) - ("confirm" [] Some(Self::Confirm)) - ("select" [i: usize] Some(Self::Select(i.expect("no index")))) - ("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path")))) - ("filter" [f: Arc] Some(Self::Filter(f.expect("no filter")))) -}); - -command!(|self: FileBrowserCommand, state: MidiPool|{ - use PoolMode::*; - use FileBrowserCommand::*; - let mode = &mut state.mode; - match mode { - Some(Import(index, ref mut browser)) => match self { - Cancel => { *mode = None; }, - Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); }, - Select(index) => { browser.index = index; }, - Confirm => if browser.is_file() { - let index = *index; - let path = browser.path(); - *mode = None; - PoolClipCommand::Import(index, path).execute(state)?; - } else if browser.is_dir() { - *mode = Some(Import(*index, browser.chdir()?)); - }, - _ => todo!(), - }, - Some(Export(index, ref mut browser)) => match self { - Cancel => { *mode = None; }, - Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); }, - Select(index) => { browser.index = index; }, - _ => unreachable!() - }, - _ => unreachable!(), - }; - None -}); - -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum ClipLengthCommand { - Begin, - Cancel, - Set(usize), - Next, - Prev, - Inc, - Dec, -} - -atom_command!(ClipLengthCommand: |state: MidiPool| { - ("begin" [] Some(Self::Begin)) - ("cancel" [] Some(Self::Cancel)) - ("next" [] Some(Self::Next)) - ("prev" [] Some(Self::Prev)) - ("inc" [] Some(Self::Inc)) - ("dec" [] Some(Self::Dec)) - ("set" [l: usize] Some(Self::Set(l.expect("no length")))) -}); - -command!(|self: ClipLengthCommand, state: MidiPool|{ - use ClipLengthCommand::*; - use ClipLengthFocus::*; - if let Some( - PoolMode::Length(clip, ref mut length, ref mut focus) - ) = state.mode_mut().clone() { - match self { - Cancel => { *state.mode_mut() = None; }, - Prev => { focus.prev() }, - Next => { focus.next() }, - Inc => match focus { - Bar => { *length += 4 * PPQ }, - Beat => { *length += PPQ }, - Tick => { *length += 1 }, - }, - Dec => match focus { - Bar => { *length = length.saturating_sub(4 * PPQ) }, - Beat => { *length = length.saturating_sub(PPQ) }, - Tick => { *length = length.saturating_sub(1) }, - }, - Set(length) => { - let old_length; - { - let clip = state.clips()[clip].clone();//.write().unwrap(); - old_length = Some(clip.read().unwrap().length); - clip.write().unwrap().length = length; - } - *state.mode_mut() = None; - return Ok(old_length.map(Self::Set)) - }, - _ => unreachable!() - } - } else { - unreachable!(); - } - None -}); diff --git a/crates/app/src/pool/pool_clips.rs b/crates/app/src/pool/pool_clips.rs deleted file mode 100644 index 65a671aa..00000000 --- a/crates/app/src/pool/pool_clips.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::*; - -pub type ClipPool = Vec>>; - -pub trait HasClips { - fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; - fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>; - fn add_clip (&self) -> (usize, Arc>) { - let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None))); - self.clips_mut().push(clip.clone()); - (self.clips().len() - 1, clip) - } -} - -#[macro_export] macro_rules! has_clips { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { - fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { - $cb.read().unwrap() - } - fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { - $cb.write().unwrap() - } - } - } -} diff --git a/crates/app/src/pool/pool_model.rs b/crates/app/src/pool/pool_model.rs deleted file mode 100644 index c6d0d335..00000000 --- a/crates/app/src/pool/pool_model.rs +++ /dev/null @@ -1,190 +0,0 @@ -use crate::*; -use super::*; - -#[derive(Debug)] -pub struct MidiPool { - pub visible: bool, - /// Collection of clips - pub clips: Arc>>>>, - /// Selected clip - pub clip: AtomicUsize, - /// Mode switch - pub mode: Option, - - pub keys: InputMap<'static, Self, PoolCommand, TuiIn, SourceIter<'static>>, - //pub keys: SourceIter<'static>, - //pub keys_rename: SourceIter<'static>, - //pub keys_length: SourceIter<'static>, - //pub keys_file: SourceIter<'static>, -} - -impl Default for MidiPool { - fn default () -> Self { - use PoolMode::*; - Self { - visible: true, - clips: Arc::from(RwLock::from(vec![])), - clip: 0.into(), - mode: None, - keys: InputMap::new(SourceIter(include_str!("../../../../config/keys_pool.edn"))) - .layer_if(|pool: &Self|matches!(pool.mode, Some(Import(..))|Some(Export(..))), - SourceIter(include_str!("../../../../config/keys_pool_file.edn"))) - .layer_if(|pool: &Self|matches!(pool.mode, Some(Rename(..))), - SourceIter(include_str!("../../../../config/keys_clip_rename.edn"))) - .layer_if(|pool: &Self|matches!(pool.mode, Some(Length(..))), - SourceIter(include_str!("../../../../config/keys_clip_length.edn"))) - } - } -} - -has_clips!(|self: MidiPool|self.clips); - -has_clip!(|self: MidiPool|self.clips().get(self.clip_index()).map(|c|c.clone())); - -from!(|clip:&Arc>|MidiPool = { - let model = Self::default(); - model.clips.write().unwrap().push(clip.clone()); - model.clip.store(1, Relaxed); - model -}); - -impl MidiPool { - pub fn clip_index (&self) -> usize { - self.clip.load(Relaxed) - } - pub fn set_clip_index (&self, value: usize) { - self.clip.store(value, Relaxed); - } - pub fn mode (&self) -> &Option { - &self.mode - } - pub fn mode_mut (&mut self) -> &mut Option { - &mut self.mode - } - pub fn begin_clip_length (&mut self) { - let length = self.clips()[self.clip_index()].read().unwrap().length; - *self.mode_mut() = Some(PoolMode::Length( - self.clip_index(), - length, - ClipLengthFocus::Bar - )); - } - pub fn begin_clip_rename (&mut self) { - let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); - *self.mode_mut() = Some(PoolMode::Rename( - self.clip_index(), - name - )); - } - pub fn begin_import (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Import( - self.clip_index(), - FileBrowser::new(None)? - )); - Ok(()) - } - pub fn begin_export (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Export( - self.clip_index(), - FileBrowser::new(None)? - )); - Ok(()) - } - pub fn new_clip (&self) -> MidiClip { - MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) - } - pub fn cloned_clip (&self) -> MidiClip { - let index = self.clip_index(); - let mut clip = self.clips()[index].read().unwrap().duplicate(); - clip.color = ItemTheme::random_near(clip.color, 0.25); - clip - } - pub fn add_new_clip (&self) -> (usize, Arc>) { - let clip = Arc::new(RwLock::new(self.new_clip())); - let index = { - let mut clips = self.clips.write().unwrap(); - clips.push(clip.clone()); - clips.len().saturating_sub(1) - }; - self.clip.store(index, Relaxed); - (index, clip) - } - pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { - let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); - if let Some(index) = index { - self.clips.write().unwrap().remove(index); - return true - } - false - } -} - -/// Displays and edits clip length. -#[derive(Clone)] -pub struct ClipLength { - /// Pulses per beat (quaver) - ppq: usize, - /// Beats per bar - bpb: usize, - /// Length of clip in pulses - pulses: usize, - /// Selected subdivision - focus: Option, -} - -impl ClipLength { - fn _new (pulses: usize, focus: Option) -> Self { - Self { ppq: PPQ, bpb: 4, pulses, focus } - } - fn bars (&self) -> usize { - self.pulses / (self.bpb * self.ppq) - } - fn beats (&self) -> usize { - (self.pulses % (self.bpb * self.ppq)) / self.ppq - } - fn ticks (&self) -> usize { - self.pulses % self.ppq - } - fn bars_string (&self) -> Arc { - format!("{}", self.bars()).into() - } - fn beats_string (&self) -> Arc { - format!("{}", self.beats()).into() - } - fn ticks_string (&self) -> Arc { - format!("{:>02}", self.ticks()).into() - } -} - -/// Focused field of `ClipLength` -#[derive(Copy, Clone, Debug)] -pub enum ClipLengthFocus { - /// Editing the number of bars - Bar, - /// Editing the number of beats - Beat, - /// Editing the number of ticks - Tick, -} - -impl ClipLengthFocus { - fn next (&mut self) { - *self = match self { Self::Bar => Self::Beat, Self::Beat => Self::Tick, Self::Tick => Self::Bar, } - } - fn prev (&mut self) { - *self = match self { Self::Bar => Self::Tick, Self::Beat => Self::Bar, Self::Tick => Self::Beat, } - } -} - -/// Modes for clip pool -#[derive(Debug, Clone)] -pub enum PoolMode { - /// Renaming a pattern - Rename(usize, Arc), - /// Editing the length of a pattern - Length(usize, usize, ClipLengthFocus), - /// Load clip from disk - Import(usize, FileBrowser), - /// Save clip to disk - Export(usize, FileBrowser), -} diff --git a/crates/app/src/pool/pool_view.rs b/crates/app/src/pool/pool_view.rs deleted file mode 100644 index a90a0838..00000000 --- a/crates/app/src/pool/pool_view.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::*; - -pub struct PoolView<'a>(pub bool, pub &'a MidiPool); - -content!(TuiOut: |self: PoolView<'a>| { - let Self(compact, model) = self; - let MidiPool { clips, .. } = self.1; - //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); - let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); - let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); - let iter = | |model.clips().clone().into_iter(); - let height = clips.read().unwrap().len() as u16; - Tui::bg(Color::Reset, Fixed::y(height, on_bg(border(Map::new(iter, move|clip: Arc>, i|{ - let item_height = 1; - let item_offset = i as u16 * item_height; - let selected = i == model.clip_index(); - let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); - let bg = if selected { color.light.rgb } else { color.base.rgb }; - let fg = color.lightest.rgb; - let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; - let length = if *compact { String::default() } else { format!("{length} ") }; - Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( - Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), - Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), - Fill::x(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), - Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), - )))) - }))))) -}); - -content!(TuiOut: |self: ClipLength| { - let bars = ||self.bars_string(); - let beats = ||self.beats_string(); - let ticks = ||self.ticks_string(); - match self.focus { - None => - row!(" ", bars(), ".", beats(), ".", ticks()), - Some(ClipLengthFocus::Bar) => - row!("[", bars(), "]", beats(), ".", ticks()), - Some(ClipLengthFocus::Beat) => - row!(" ", bars(), "[", beats(), "]", ticks()), - Some(ClipLengthFocus::Tick) => - row!(" ", bars(), ".", beats(), "[", ticks()), - } -}); diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index ec030508..c3b8a907 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -2,7 +2,7 @@ use crate::*; pub(crate) use std::fmt::Write; pub(crate) use ::tengri::tui::ratatui::prelude::Position; -view!(TuiOut: |self: Tek| self.size.of(View(self, self.view)); { +view!(TuiOut: |self: Tek| self.size.of(View(self, self.config.view)); { ":nil" => Box::new("nil"), ":modal" => self.view_modal(), ":status" => self.view_status(), @@ -42,7 +42,7 @@ impl Tek { } fn view_modal_help (&self) -> impl Content + use<'_> { - let bindings = ||self.keys.layers.iter() + let bindings = ||self.config.keys.layers.iter() .filter_map(|a|(a.0)(self).then_some(a.1)) .flat_map(|a|a) .filter_map(|x|if let Value::Exp(_, iter)=x.value{ @@ -388,16 +388,15 @@ impl<'a> ArrangerView<'a> { let queued = track.player.next_clip.is_some(); let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ ")); let queued_clip = Thunk::new(||{ - let title = if let Some((_, clip)) = track.player.next_clip.as_ref() { + Tui::bg(Reset, if let Some((_, clip)) = track.player.next_clip.as_ref() { if let Some(clip) = clip { - clip.read().unwrap().name.as_ref().clone() + clip.read().unwrap().name.clone() } else { - "Stop" + "Stop".into() } } else { - "" - }; - Tui::bg(Reset, title) + "".into() + }) }); Either(queued, queued_clip, queued_blank) })) @@ -917,3 +916,408 @@ impl ViewCache { } } } + +pub struct PoolView<'a>(pub bool, pub &'a MidiPool); + +content!(TuiOut: |self: PoolView<'a>| { + let Self(compact, model) = self; + let MidiPool { clips, .. } = self.1; + //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); + let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); + let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); + let iter = | |model.clips().clone().into_iter(); + let height = clips.read().unwrap().len() as u16; + Tui::bg(Color::Reset, Fixed::y(height, on_bg(border(Map::new(iter, move|clip: Arc>, i|{ + let item_height = 1; + let item_offset = i as u16 * item_height; + let selected = i == model.clip_index(); + let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); + let bg = if selected { color.light.rgb } else { color.base.rgb }; + let fg = color.lightest.rgb; + let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; + let length = if *compact { String::default() } else { format!("{length} ") }; + Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( + Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), + Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), + Fill::x(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), + Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), + )))) + }))))) +}); + +content!(TuiOut: |self: ClipLength| { + let bars = ||self.bars_string(); + let beats = ||self.beats_string(); + let ticks = ||self.ticks_string(); + match self.focus { + None => + row!(" ", bars(), ".", beats(), ".", ticks()), + Some(ClipLengthFocus::Bar) => + row!("[", bars(), "]", beats(), ".", ticks()), + Some(ClipLengthFocus::Beat) => + row!(" ", bars(), "[", beats(), "]", ticks()), + Some(ClipLengthFocus::Tick) => + row!(" ", bars(), ".", beats(), "[", ticks()), + } +}); + +/// A clip, rendered as a horizontal piano roll. +#[derive(Clone)] +pub struct PianoHorizontal { + pub clip: Option>>, + /// Buffer where the whole clip is rerendered on change + pub buffer: Arc>, + /// Size of actual notes area + pub size: Measure, + /// The display window + pub range: MidiRangeModel, + /// The note cursor + pub point: MidiPointModel, + /// The highlight color palette + pub color: ItemTheme, + /// Width of the keyboard + pub keys_width: u16, +} + +impl PianoHorizontal { + pub fn new (clip: Option<&Arc>>) -> Self { + let size = Measure::new(); + let mut range = MidiRangeModel::from((12, true)); + range.time_axis = size.x.clone(); + range.note_axis = size.y.clone(); + let piano = Self { + keys_width: 5, + size, + range, + buffer: RwLock::new(Default::default()).into(), + point: MidiPointModel::default(), + clip: clip.cloned(), + color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), + }; + piano.redraw(); + piano + } +} + +pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator { + (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) +} + +content!(TuiOut:|self: PianoHorizontal| Tui::bg(Tui::g(40), Bsp::s( + Bsp::e( + Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())), + self.timeline() + ), + Bsp::e( + self.keys(), + self.size.of(Tui::bg(Tui::g(32), Bsp::b( + Fill::xy(self.notes()), + Fill::xy(self.cursor()), + ))) + ), +))); + +impl PianoHorizontal { + /// Draw the piano roll background. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize) { + for (y, note) in (0..=127).rev().enumerate() { + for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { + let cell = buf.get_mut(x, y).unwrap(); + cell.set_bg(clip.color.darkest.rgb); + if time % 384 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('│'); + } else if time % 96 == 0 { + cell.set_fg(clip.color.dark.rgb); + cell.set_char('╎'); + } else if time % note_len == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('┊'); + } else if (127 - note) % 12 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('='); + } else if (127 - note) % 6 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('—'); + } else { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('·'); + } + } + } + } + /// Draw the piano roll foreground. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { + let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); + let mut notes_on = [false;128]; + for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { + for (_y, note) in (0..=127).rev().enumerate() { + if let Some(cell) = buf.get_mut(x, note) { + if notes_on[note] { + cell.set_char('▂'); + cell.set_style(style); + } + } + } + let time_end = time_start + zoom; + for time in time_start..time_end.min(clip.length) { + for event in clip.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + if let Some(cell) = buf.get_mut(x, note) { + cell.set_char('█'); + cell.set_style(style); + } + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} + } + } + } + + } + } + fn notes (&self) -> impl Content { + let time_start = self.time_start().get(); + let note_lo = self.note_lo().get(); + let note_hi = self.note_hi(); + let buffer = self.buffer.clone(); + ThunkRender::new(move|to: &mut TuiOut|{ + let source = buffer.read().unwrap(); + let [x0, y0, w, _h] = to.area().xywh(); + //if h as usize != note_axis { + //panic!("area height mismatch: {h} <> {note_axis}"); + //} + for (area_x, screen_x) in (x0..x0+w).enumerate() { + for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { + let source_x = time_start + area_x; + let source_y = note_hi - area_y; + // TODO: enable loop rollover: + //let source_x = (time_start + area_x) % source.width.max(1); + //let source_y = (note_hi - area_y) % source.height.max(1); + let is_in_x = source_x < source.width; + let is_in_y = source_y < source.height; + if is_in_x && is_in_y { + if let Some(source_cell) = source.get(source_x, source_y) { + if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { + *cell = source_cell.clone(); + } + } + } + } + } + }) + } + fn cursor (&self) -> impl Content { + let style = Some(Style::default().fg(self.color.lightest.rgb)); + let note_hi = self.note_hi(); + let note_lo = self.note_lo().get(); + let note_pos = self.note_pos(); + let note_len = self.note_len(); + let time_pos = self.time_pos(); + let time_start = self.time_start().get(); + let time_zoom = self.time_zoom().get(); + ThunkRender::new(move|to: &mut TuiOut|{ + let [x0, y0, w, _] = to.area().xywh(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + if note == note_pos { + for x in 0..w { + let screen_x = x0 + x; + let time_1 = time_start + x as usize * time_zoom; + let time_2 = time_1 + time_zoom; + if time_1 <= time_pos && time_pos < time_2 { + to.blit(&"█", screen_x, screen_y, style); + let tail = note_len as u16 / time_zoom as u16; + for x_tail in (screen_x + 1)..(screen_x + tail) { + to.blit(&"▂", x_tail, screen_y, style); + } + break + } + } + break + } + } + }) + } + fn keys (&self) -> impl Content { + let state = self; + let color = state.color; + let note_lo = state.note_lo().get(); + let note_hi = state.note_hi(); + let note_pos = state.note_pos(); + let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); + let off_style = Some(Style::default().fg(Tui::g(255))); + let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); + Fill::y(Fixed::x(self.keys_width, ThunkRender::new(move|to: &mut TuiOut|{ + let [x, y0, _w, _h] = to.area().xywh(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + to.blit(&to_key(note), x, screen_y, key_style); + if note > 127 { + continue + } + if note == note_pos { + to.blit(&format!("{:<5}", Note::pitch_to_name(note)), x, screen_y, on_style) + } else { + to.blit(&Note::pitch_to_name(note), x, screen_y, off_style) + }; + } + }))) + } + fn timeline (&self) -> impl Content + '_ { + Fill::x(Fixed::y(1, ThunkRender::new(move|to: &mut TuiOut|{ + let [x, y, w, _h] = to.area(); + let style = Some(Style::default().dim()); + let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { + let t = area_x as usize * self.time_zoom().get(); + if t < length { + to.blit(&"|", screen_x, y, style); + } + } + }))) + } +} + +has_size!(|self:PianoHorizontal|&self.size); + +impl TimeRange for PianoHorizontal { + fn time_len (&self) -> &AtomicUsize { self.range.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.range.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } +} + +impl NoteRange for PianoHorizontal { + fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } +} + +impl NotePoint for PianoHorizontal { + fn note_len (&self) -> usize { self.point.note_len() } + fn set_note_len (&self, x: usize) -> usize { self.point.set_note_len(x) } + fn note_pos (&self) -> usize { self.point.note_pos() } + fn set_note_pos (&self, x: usize) -> usize { self.point.set_note_pos(x) } +} + +impl TimePoint for PianoHorizontal { + fn time_pos (&self) -> usize { self.point.time_pos() } + fn set_time_pos (&self, x: usize) -> usize { self.point.set_time_pos(x) } +} + +impl MidiViewer for PianoHorizontal { + fn clip (&self) -> &Option>> { + &self.clip + } + fn clip_mut (&mut self) -> &mut Option>> { + &mut self.clip + } + /// Determine the required space to render the clip. + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { + (clip.length / self.range.time_zoom().get(), 128) + } + fn redraw (&self) { + *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { + let clip = clip.read().unwrap(); + let buf_size = self.buffer_size(&clip); + let mut buffer = BigBuffer::from(buf_size); + let note_len = self.note_len(); + let time_zoom = self.time_zoom().get(); + self.time_len().set(clip.length); + PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom, note_len); + PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); + buffer + } else { + Default::default() + } + } + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.color = clip.map(|p|p.read().unwrap().color) + .unwrap_or(ItemTheme::G[64]); + self.redraw(); + } +} + +impl std::fmt::Debug for PianoHorizontal { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + let buffer = self.buffer.read().unwrap(); + f.debug_struct("PianoHorizontal") + .field("time_zoom", &self.range.time_zoom) + .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) + .finish() + } +} + // Update sequencer playhead indicator + //self.now().set(0.); + //if let Some((ref started_at, Some(ref playing))) = self.player.play_clip { + //let clip = clip.read().unwrap(); + //if *playing.read().unwrap() == *clip { + //let pulse = self.current().pulse.get(); + //let start = started_at.pulse.get(); + //let now = (pulse - start) % clip.length as f64; + //self.now().set(now); + //} + //} + +fn to_key (note: usize) -> &'static str { + match note % 12 { + 11 => "████▌", + 10 => " ", + 9 => "████▌", + 8 => " ", + 7 => "████▌", + 6 => " ", + 5 => "████▌", + 4 => "████▌", + 3 => " ", + 2 => "████▌", + 1 => " ", + 0 => "████▌", + _ => unreachable!(), + } +} + +pub struct OctaveVertical { + on: [bool; 12], + colors: [Color; 3] +} + +impl Default for OctaveVertical { + fn default () -> Self { + Self { + on: [false; 12], + colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] + } + } +} + +impl OctaveVertical { + fn color (&self, pitch: usize) -> Color { + let pitch = pitch % 12; + self.colors[if self.on[pitch] { 2 } else { + match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } + }] + } +} + +impl Content for OctaveVertical { + fn content (&self) -> impl Render { + row!( + Tui::fg_bg(self.color(0), self.color(1), "▙"), + Tui::fg_bg(self.color(2), self.color(3), "▙"), + Tui::fg_bg(self.color(4), self.color(5), "▌"), + Tui::fg_bg(self.color(6), self.color(7), "▟"), + Tui::fg_bg(self.color(8), self.color(9), "▟"), + Tui::fg_bg(self.color(10), self.color(11), "▟"), + ) + } +} diff --git a/crates/cli/tek.rs b/crates/cli/tek.rs index bf0cd6d6..a5990ad0 100644 --- a/crates/cli/tek.rs +++ b/crates/cli/tek.rs @@ -100,58 +100,20 @@ impl Cli { for (index, connect) in midi_tos.iter().enumerate() { let port = JackMidiOut::new(jack, &format!("{index}/M"), &[connect.clone()])?; midi_outs.push(port); - } + }; + let config_path = match mode { + LaunchMode::Clock => "config/config_transport.edn", + LaunchMode::Sequencer => "config/config_sequencer.edn", + LaunchMode::Groovebox => "config/config_groovebox.edn", + LaunchMode::Arranger { .. } => "config/config_arranger.edn", + LaunchMode::Sampler => "config/config_sampler.edn", + _ => todo!("{mode:?}"), + }; + let config = Configuration::new(&config_path, false)?; let mut app = Tek { jack: jack.clone(), color: ItemTheme::random(), clock: Clock::new(jack, self.bpm)?, - view: SourceIter(match mode { - LaunchMode::Clock => - include_str!("../../config/view_transport.edn"), - LaunchMode::Sequencer => - include_str!("../../config/view_sequencer.edn"), - LaunchMode::Groovebox => - include_str!("../../config/view_groovebox.edn"), - LaunchMode::Arranger { .. } => - include_str!("../../config/view_arranger.edn"), - LaunchMode::Sampler => - include_str!("../../config/view_sampler.edn"), - _ => todo!("{mode:?}"), - }), - keys: match mode { - LaunchMode::Sampler => InputMap::default() - .layer(SourceIter(include_str!("../../config/keys_global.edn")).into()) - .layer(SourceIter(include_str!("../../config/keys_sampler.edn")).into()), - LaunchMode::Clock => InputMap::default() - .layer(SourceIter(include_str!("../../config/keys_global.edn")).into()) - .layer(SourceIter(include_str!("../../config/keys_clock.edn")).into()), - LaunchMode::Sequencer => InputMap::default() - .layer(SourceIter(include_str!("../../config/keys_global.edn")).into()) - .layer(SourceIter(include_str!("../../config/keys_editor.edn")).into()) - .layer(SourceIter(include_str!("../../config/keys_clock.edn")).into()) - .layer(SourceIter(include_str!("../../config/keys_sequencer.edn")).into()), - LaunchMode::Groovebox => InputMap::default() - .layer(SourceIter(include_str!("../../config/keys_global.edn")).into()) - .layer(SourceIter(include_str!("../../config/keys_editor.edn")).into()) - .layer(SourceIter(include_str!("../../config/keys_clock.edn")).into()) - .layer(SourceIter(include_str!("../../config/keys_sequencer.edn")).into()) - .layer(SourceIter(include_str!("../../config/keys_groovebox.edn")).into()), - LaunchMode::Arranger {..} => InputMap::default() - .layer(SourceIter(include_str!("../../config/keys_global.edn")).into()) - .layer_if(|state: &Tek|state.is_editing(), - SourceIter(include_str!("../../config/keys_editor.edn")).into()) - .layer_if(|state: &Tek|state.selected.is_clip()&&!state.is_editing(), - SourceIter(include_str!("../../config/keys_clip.edn")).into()) - .layer_if(|state: &Tek|state.selected.is_track()&&!state.is_editing(), - SourceIter(include_str!("../../config/keys_track.edn")).into()) - .layer_if(|state: &Tek|state.selected.is_scene()&&!state.is_editing(), - SourceIter(include_str!("../../config/keys_scene.edn")).into()) - .layer_if(|state: &Tek|state.selected.is_mix()&&!state.is_editing(), - SourceIter(include_str!("../../config/keys_mix.edn")).into()) - .layer(SourceIter(include_str!("../../config/keys_clock.edn")).into()) - .layer(SourceIter(include_str!("../../config/keys_arranger.edn")).into()), - _ => todo!("{mode:?}"), - }, pool: match mode { LaunchMode::Sequencer | LaunchMode::Groovebox => clip.as_ref().map(Into::into), LaunchMode::Arranger { .. } => Some(Default::default()), @@ -186,6 +148,7 @@ impl Cli { }, scenes, selected: Selection::TrackClip { track: 0, scene: 0 }, + config, ..Default::default() }; if let &LaunchMode::Arranger { scenes, tracks, track_width, .. } = mode { diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml index 669f1efc..ef59f3ba 100644 --- a/crates/device/Cargo.toml +++ b/crates/device/Cargo.toml @@ -13,7 +13,12 @@ wavers = { workspace = true, optional = true } [features] default = [ "clock", "sequencer", "sampler" ] -lv2 = [ "livi" ] +clock = [] sampler = [ "symphonia", "wavers" ] sequencer = [ "clock", "uuid" ] -clock = [] +plugin = [] # temporary +lv2 = [ "livi" ] +vst2 = [] +vst3 = [] +clap = [] +sf2 = [] diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 2531f35a..6c786213 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -29,10 +29,12 @@ pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line} #[derive(Debug)] pub enum Device { - #[cfg(feature = "sequencer")] - Sequencer(MidiPlayer), - #[cfg(feature = "sampler")] - Sampler(Sampler), - #[cfg(feature = "plugin")] - Plugin(Plugin), + #[cfg(feature = "sequencer")] Sequencer(MidiPlayer), + #[cfg(feature = "sampler")] Sampler(Sampler), + #[cfg(feature = "plugin")] Plugin(Plugin), + #[cfg(feature = "lv2")] Lv2, // TODO + #[cfg(feature = "vst2")] Vst2, // TODO + #[cfg(feature = "vst3")] Vst3, // TODO + #[cfg(feature = "clap")] Clap, // TODO + #[cfg(feature = "sf2")] Sf2, // TODO } diff --git a/crates/engine/src/jack.rs b/crates/engine/src/jack.rs index bcc64a04..b13d667e 100644 --- a/crates/engine/src/jack.rs +++ b/crates/engine/src/jack.rs @@ -15,4 +15,3 @@ pub(crate) use std::sync::{Arc, RwLock}; mod jack_client; pub use self::jack_client::*; mod jack_event; pub use self::jack_event::*; mod jack_port; pub use self::jack_port::*; - diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index da26acb2..b4840a09 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1,7 +1,7 @@ #![feature(type_alias_impl_trait)] -mod time; pub use self::time::*; -mod note; pub use self::note::*; +mod time; pub use self::time::*; +mod note; pub use self::note::*; pub mod jack; pub use self::jack::*; pub mod midi; pub use self::midi::*; @@ -10,11 +10,7 @@ pub(crate) use std::path::PathBuf; pub(crate) use std::fmt::Debug; pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem}; -pub(crate) use ::tengri::input::*; -pub(crate) use ::tengri::output::*; -pub(crate) use ::tengri::dsl::*; pub(crate) use ::tengri::tui::*; -pub(crate) use ::tengri::tui::ratatui::style::{Style, Stylize, Color}; pub use ::atomic_float; pub(crate) use atomic_float::*; diff --git a/crates/engine/src/midi.rs b/crates/engine/src/midi.rs index 62d42608..b58425c8 100644 --- a/crates/engine/src/midi.rs +++ b/crates/engine/src/midi.rs @@ -1,6 +1,8 @@ use crate::*; pub use ::midly::{ + Smf, + TrackEventKind, MidiMessage, num::*, live::*, diff --git a/deps/tengri b/deps/tengri index 2b208e3c..0d4ba4a5 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit 2b208e3c497d595f7dad5a6d190dfe08f7fb9dc0 +Subproject commit 0d4ba4a54ef0528a0a45b58e21a1e4aa7ed5eaf9