diff --git a/crates/app/src/keys.rs b/crates/app/src/keys.rs index 753ca059..c2ec5da7 100644 --- a/crates/app/src/keys.rs +++ b/crates/app/src/keys.rs @@ -36,6 +36,9 @@ pub fn handle_arranger (app: &mut Tek, input: &TuiIn) -> Perhaps { } pub fn handle_sequencer (app: &mut Tek, input: &TuiIn) -> Perhaps { + if app.editor.handle(input)? == Some(true) { + return Ok(Some(true)) + } if let Some(command) = SourceIter(include_str!("../edn/sequencer_keys.edn")) .command::<_, TekCommand, _>(app, input) { @@ -48,6 +51,9 @@ pub fn handle_sequencer (app: &mut Tek, input: &TuiIn) -> Perhaps { } pub fn handle_groovebox (app: &mut Tek, input: &TuiIn) -> Perhaps { + if app.editor.handle(input)? == Some(true) { + return Ok(Some(true)) + } if let Some(command) = SourceIter(include_str!("../edn/groovebox_keys.edn")) .command::<_, TekCommand, _>(app, input) { diff --git a/crates/midi/midi.scratch.rs b/crates/midi/midi.scratch.rs new file mode 100644 index 00000000..cdd1807e --- /dev/null +++ b/crates/midi/midi.scratch.rs @@ -0,0 +1,86 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +//fn to_clips_command (state: &MidiPool, input: &Event) -> Option { + //use KeyCode::{Up, Down, Delete, Char}; + //use PoolCommand as Cmd; + //let index = state.clip_index(); + //let count = state.clips().len(); + //Some(match input { + //kpat!(Char('n')) => Cmd::Rename(ClipRenameCommand::Begin), + //kpat!(Char('t')) => Cmd::Length(ClipLengthCommand::Begin), + //kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin), + //kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin), + //kpat!(Char('c')) => Cmd::Clip(PoolClipCommand::SetColor(index, ItemColor::random())), + //kpat!(Char('[')) | kpat!(Up) => Cmd::Select( + //index.overflowing_sub(1).0.min(state.clips().len() - 1) + //), + //kpat!(Char(']')) | kpat!(Down) => Cmd::Select( + //index.saturating_add(1) % state.clips().len() + //), + //kpat!(Char('<')) => if index > 1 { + //state.set_clip_index(state.clip_index().saturating_sub(1)); + //Cmd::Clip(PoolClipCommand::Swap(index - 1, index)) + //} else { + //return None + //}, + //kpat!(Char('>')) => if index < count.saturating_sub(1) { + //state.set_clip_index(state.clip_index() + 1); + //Cmd::Clip(PoolClipCommand::Swap(index + 1, index)) + //} else { + //return None + //}, + //kpat!(Delete) => if index > 0 { + //state.set_clip_index(index.min(count.saturating_sub(1))); + //Cmd::Clip(PoolClipCommand::Delete(index)) + //} else { + //return None + //}, + //kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(PoolClipCommand::Add(count, MidiClip::new( + //"Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) + //))), + //kpat!(Char('i')) => Cmd::Clip(PoolClipCommand::Add(index + 1, MidiClip::new( + //"Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) + //))), + //kpat!(Char('d')) | kpat!(Shift-Char('D')) => { + //let mut clip = state.clips()[index].read().unwrap().duplicate(); + //clip.color = ItemPalette::random_near(clip.color, 0.25); + //Cmd::Clip(PoolClipCommand::Add(index + 1, clip)) + //}, + //_ => return None + //}) +//} +//keymap!(KEYS_MIDI_EDITOR = |s: MidiEditor, _input: Event| MidiEditCommand { + //key(Up) => SetNoteCursor(s.note_pos() + 1), + //key(Char('w')) => SetNoteCursor(s.note_pos() + 1), + //key(Down) => SetNoteCursor(s.note_pos().saturating_sub(1)), + //key(Char('s')) => SetNoteCursor(s.note_pos().saturating_sub(1)), + //key(Left) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())), + //key(Char('a')) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())), + //key(Right) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()), + //ctrl(alt(key(Up))) => SetNoteScroll(s.note_pos() + 3), + //ctrl(alt(key(Down))) => SetNoteScroll(s.note_pos().saturating_sub(3)), + //ctrl(alt(key(Left))) => SetTimeScroll(s.time_pos().saturating_sub(s.time_zoom().get())), + //ctrl(alt(key(Right))) => SetTimeScroll((s.time_pos() + s.time_zoom().get()) % s.clip_length()), + //ctrl(key(Up)) => SetNoteScroll(s.note_lo().get() + 1), + //ctrl(key(Down)) => SetNoteScroll(s.note_lo().get().saturating_sub(1)), + //ctrl(key(Left)) => SetTimeScroll(s.time_start().get().saturating_sub(s.note_len())), + //ctrl(key(Right)) => SetTimeScroll(s.time_start().get() + s.note_len()), + //alt(key(Up)) => SetNoteCursor(s.note_pos() + 3), + //alt(key(Down)) => SetNoteCursor(s.note_pos().saturating_sub(3)), + //alt(key(Left)) => SetTimeCursor(s.time_pos().saturating_sub(s.time_zoom().get())), + //alt(key(Right)) => SetTimeCursor((s.time_pos() + s.time_zoom().get()) % s.clip_length()), + //key(Char('d')) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()), + //key(Char('z')) => SetTimeLock(!s.time_lock().get()), + //key(Char('-')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }), + //key(Char('_')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }), + //key(Char('=')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }), + //key(Char('+')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }), + //key(Enter) => PutNote, + //ctrl(key(Enter)) => AppendNote, + //key(Char(',')) => SetNoteLength(NoteDuration::prev(s.note_len())), + //key(Char('.')) => SetNoteLength(NoteDuration::next(s.note_len())), + //key(Char('<')) => SetNoteLength(NoteDuration::prev(s.note_len())), + //key(Char('>')) => SetNoteLength(NoteDuration::next(s.note_len())), + ////// TODO: kpat!(Char('/')) => // toggle 3plet + ////// TODO: kpat!(Char('?')) => // toggle dotted +//}); + diff --git a/crates/midi/src/clip.rs b/crates/midi/src/clip.rs new file mode 100644 index 00000000..ce78907b --- /dev/null +++ b/crates/midi/src/clip.rs @@ -0,0 +1,52 @@ +mod clip_editor; pub use self::clip_editor::*; +mod clip_launch; pub use self::clip_launch::*; +mod clip_model; pub use self::clip_model::*; +mod clip_play; pub use self::clip_play::*; +mod clip_view; pub use self::clip_view::*; + +#[cfg(test)] #[test] pub fn test_midi_clip () { + let clip = MidiClip::stop_all(); + println!("{clip:?}"); + + let clip = MidiClip::default(); + let mut clip = MidiClip::new("clip", true, 1, None, None); + clip.set_length(96); + clip.toggle_loop(); + clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); + assert!(clip.contains_note_on(36.into(), 6, 18)); + assert_eq!(&clip.notes, &clip.duplicate().notes); + + let clip = std::sync::Arc::new(clip); + assert_eq!(clip.clone(), clip); +} + +#[cfg(test)] #[test] fn test_midi_play () { + let player = MidiPlayer::default(); + println!("{player:?}"); +} + +#[cfg(test)] #[test] fn test_midi_edit () { + let editor = MidiEditor::default(); + let mut editor = MidiEditor { + mode: PianoHorizontal::new(Some(&Arc::new(RwLock::new(MidiClip::stop_all())))), + size: Default::default(), + keys: Default::default(), + }; + let _ = editor.put_note(true); + let _ = editor.put_note(false); + let _ = editor.clip_status(); + let _ = editor.edit_status(); + struct TestEditorHost(Option); + has_editor!(|self: TestEditorHost|{ + editor = self.0; + editor_w = 0; + editor_h = 0; + is_editing = false; + }); + let mut host = TestEditorHost(Some(editor)); + let _ = host.editor(); + let _ = host.editor_mut(); + let _ = host.is_editing(); + let _ = host.editor_w(); + let _ = host.editor_h(); +} diff --git a/crates/midi/src/midi_edit.rs b/crates/midi/src/clip/clip_editor.rs similarity index 72% rename from crates/midi/src/midi_edit.rs rename to crates/midi/src/clip/clip_editor.rs index ea97b9ef..b6090442 100644 --- a/crates/midi/src/midi_edit.rs +++ b/crates/midi/src/clip/clip_editor.rs @@ -1,62 +1,60 @@ //! MIDI editor. use crate::*; -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 { pub mode: PianoHorizontal, pub size: Measure, keys: 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 { + mode: PianoHorizontal::new(None), + size: Measure::new(), + keys: SourceIter(include_str!("../../edn/keys_edit.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 }); + 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(), @@ -80,22 +78,7 @@ provide!(usize: |self: MidiEditor| { ":time-zoom-next" => self.time_zoom().get() + 1, ":time-zoom-prev" => self.time_zoom().get().saturating_sub(1).max(1), }); -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 { - mode: PianoHorizontal::new(None), - size: Measure::new(), - keys: SourceIter(KEYS_EDIT), - } - } -} + impl MidiEditor { //fn clip_length (&self) -> usize { //self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) @@ -155,6 +138,7 @@ impl MidiEditor { ) } } + impl TimeRange for MidiEditor { fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } @@ -162,20 +146,24 @@ impl TimeRange for MidiEditor { 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) { self.mode.set_note_len(x) } fn note_pos (&self) -> usize { self.mode.note_pos() } fn set_note_pos (&self, x: 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) { 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() } @@ -183,6 +171,7 @@ impl MidiViewer for MidiEditor { fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } } + atom_command!(MidiEditCommand: |state: MidiEditor| { ("note/append" [] Some(Self::AppendNote)) ("note/put" [] Some(Self::PutNote)) @@ -194,6 +183,7 @@ atom_command!(MidiEditCommand: |state: MidiEditor| { ("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, @@ -208,6 +198,7 @@ atom_command!(MidiEditCommand: |state: MidiEditor| { SetTimeLock(bool), Show(Option>>), } + handle!(TuiIn: |self: MidiEditor, input|{ Ok(if let Some(command) = self.keys.command::<_, MidiEditCommand, _>(self, input) { let _undo = command.execute(self)?; @@ -216,6 +207,7 @@ handle!(TuiIn: |self: MidiEditor, input|{ None }) }); + impl Command for MidiEditCommand { fn execute (self, state: &mut MidiEditor) -> Perhaps { use MidiEditCommand::*; @@ -244,63 +236,32 @@ impl Command for MidiEditCommand { } } -//keymap!(KEYS_MIDI_EDITOR = |s: MidiEditor, _input: Event| MidiEditCommand { - //key(Up) => SetNoteCursor(s.note_pos() + 1), - //key(Char('w')) => SetNoteCursor(s.note_pos() + 1), - //key(Down) => SetNoteCursor(s.note_pos().saturating_sub(1)), - //key(Char('s')) => SetNoteCursor(s.note_pos().saturating_sub(1)), - //key(Left) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())), - //key(Char('a')) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())), - //key(Right) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()), - //ctrl(alt(key(Up))) => SetNoteScroll(s.note_pos() + 3), - //ctrl(alt(key(Down))) => SetNoteScroll(s.note_pos().saturating_sub(3)), - //ctrl(alt(key(Left))) => SetTimeScroll(s.time_pos().saturating_sub(s.time_zoom().get())), - //ctrl(alt(key(Right))) => SetTimeScroll((s.time_pos() + s.time_zoom().get()) % s.clip_length()), - //ctrl(key(Up)) => SetNoteScroll(s.note_lo().get() + 1), - //ctrl(key(Down)) => SetNoteScroll(s.note_lo().get().saturating_sub(1)), - //ctrl(key(Left)) => SetTimeScroll(s.time_start().get().saturating_sub(s.note_len())), - //ctrl(key(Right)) => SetTimeScroll(s.time_start().get() + s.note_len()), - //alt(key(Up)) => SetNoteCursor(s.note_pos() + 3), - //alt(key(Down)) => SetNoteCursor(s.note_pos().saturating_sub(3)), - //alt(key(Left)) => SetTimeCursor(s.time_pos().saturating_sub(s.time_zoom().get())), - //alt(key(Right)) => SetTimeCursor((s.time_pos() + s.time_zoom().get()) % s.clip_length()), - //key(Char('d')) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()), - //key(Char('z')) => SetTimeLock(!s.time_lock().get()), - //key(Char('-')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }), - //key(Char('_')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }), - //key(Char('=')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }), - //key(Char('+')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }), - //key(Enter) => PutNote, - //ctrl(key(Enter)) => AppendNote, - //key(Char(',')) => SetNoteLength(NoteDuration::prev(s.note_len())), - //key(Char('.')) => SetNoteLength(NoteDuration::next(s.note_len())), - //key(Char('<')) => SetNoteLength(NoteDuration::prev(s.note_len())), - //key(Char('>')) => SetNoteLength(NoteDuration::next(s.note_len())), - ////// TODO: kpat!(Char('/')) => // toggle 3plet - ////// TODO: kpat!(Char('?')) => // toggle dotted -//}); - -#[cfg(test)] #[test] fn test_midi_edit () { - let mut editor = MidiEditor { - mode: PianoHorizontal::new(Some(&Arc::new(RwLock::new(MidiClip::stop_all())))), - size: Default::default(), - keys: Default::default(), - }; - let _ = editor.put_note(true); - let _ = editor.put_note(false); - let _ = editor.clip_status(); - let _ = editor.edit_status(); - struct TestEditorHost(Option); - has_editor!(|self: TestEditorHost|{ - editor = self.0; - editor_w = 0; - editor_h = 0; - is_editing = false; - }); - let mut host = TestEditorHost(Some(editor)); - let _ = host.editor(); - let _ = host.editor_mut(); - let _ = host.is_editing(); - let _ = host.editor_w(); - let _ = host.editor_h(); +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/midi/src/midi_launch.rs b/crates/midi/src/clip/clip_launch.rs similarity index 99% rename from crates/midi/src/midi_launch.rs rename to crates/midi/src/clip/clip_launch.rs index ab6702af..1d87cdfb 100644 --- a/crates/midi/src/midi_launch.rs +++ b/crates/midi/src/clip/clip_launch.rs @@ -1,12 +1,19 @@ use crate::*; pub trait HasPlayClip: HasClock { + fn reset (&self) -> bool; + fn reset_mut (&mut self) -> &mut bool; + fn play_clip (&self) -> &Option<(Moment, Option>>)>; + fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + fn next_clip (&self) -> &Option<(Moment, Option>>)>; + fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + fn pulses_since_start (&self) -> Option { if let Some((started, Some(_))) = self.play_clip().as_ref() { let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); @@ -15,6 +22,7 @@ pub trait HasPlayClip: HasClock { None } } + fn pulses_since_start_looped (&self) -> Option<(f64, f64)> { if let Some((started, Some(clip))) = self.play_clip().as_ref() { let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); @@ -26,12 +34,14 @@ pub trait HasPlayClip: HasClock { None } } + fn enqueue_next (&mut self, clip: Option<&Arc>>) { let start = self.clock().next_launch_pulse() as f64; let instant = Moment::from_pulse(self.clock().timebase(), start); *self.next_clip_mut() = Some((instant, clip.cloned())); *self.reset_mut() = true; } + fn play_status (&self) -> impl Content { let (name, color): (Arc, ItemPalette) = if let Some((_, Some(clip))) = self.play_clip() { let MidiClip { ref name, color, .. } = *clip.read().unwrap(); @@ -44,6 +54,7 @@ pub trait HasPlayClip: HasClock { .unwrap_or_else(||String::from(" ")).into(); FieldV(color, "Now:", format!("{} {}", time, name)) } + fn next_status (&self) -> impl Content { let mut time: Arc = String::from("--.-.--").into(); let mut name: Arc = String::from("").into(); @@ -81,4 +92,5 @@ pub trait HasPlayClip: HasClock { }; FieldV(color, "Next:", format!("{} {}", time, name)) } + } diff --git a/crates/midi/src/midi_clip.rs b/crates/midi/src/clip/clip_model.rs similarity index 100% rename from crates/midi/src/midi_clip.rs rename to crates/midi/src/clip/clip_model.rs diff --git a/crates/midi/src/midi_player.rs b/crates/midi/src/clip/clip_play.rs similarity index 100% rename from crates/midi/src/midi_player.rs rename to crates/midi/src/clip/clip_play.rs diff --git a/crates/midi/src/midi_view.rs b/crates/midi/src/clip/clip_view.rs similarity index 100% rename from crates/midi/src/midi_view.rs rename to crates/midi/src/clip/clip_view.rs diff --git a/crates/midi/src/lib.rs b/crates/midi/src/lib.rs index c3f1e713..a617b9b3 100644 --- a/crates/midi/src/lib.rs +++ b/crates/midi/src/lib.rs @@ -1,86 +1,21 @@ -mod midi_clip; pub use midi_clip::*; -mod midi_edit; pub use midi_edit::*; -mod midi_in; pub use midi_in::*; -mod midi_launch; pub use midi_launch::*; -mod midi_out; pub use midi_out::*; -mod midi_pitch; pub use midi_pitch::*; -mod midi_player; pub use midi_player::*; -mod midi_point; pub use midi_point::*; -mod midi_pool; pub use midi_pool::*; -mod midi_range; pub use midi_range::*; -mod midi_view; pub use midi_view::*; -mod piano_h; pub use self::piano_h::*; -mod piano_v; pub use self::piano_v::*; - -pub(crate) use ::tek_time::*; -pub(crate) use ::tek_jack::{*, jack::*}; -pub(crate) use ::tengri::{ - input::*, - output::*, - dsl::*, - tui::{ - *, - ratatui::style::{Style, Stylize, Color} - } -}; - pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, AtomicBool, Ordering::Relaxed}}; pub(crate) use std::path::PathBuf; pub(crate) use std::fmt::Debug; -pub use ::midly; pub(crate) use ::midly::{*, num::*, live::*}; +pub use ::midly; +pub(crate) use ::midly::{*, num::*, live::*}; -pub(crate) const KEYS_EDIT: &str = include_str!("../edn/keys_edit.edn"); -pub(crate) const KEYS_POOL: &str = include_str!("../edn/keys_pool.edn"); -pub(crate) const KEYS_FILE: &str = include_str!("../edn/keys_pool_file.edn"); -pub(crate) const KEYS_LENGTH: &str = include_str!("../edn/keys_clip_length.edn"); -pub(crate) const KEYS_RENAME: &str = include_str!("../edn/keys_clip_rename.edn"); +pub(crate) use ::tek_time::*; +pub(crate) use ::tek_jack::{*, jack::*}; +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}; -/// Add "all notes off" to the start of a buffer. -pub fn all_notes_off (output: &mut [Vec>]) { - let mut buf = vec![]; - let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; - let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; - evt.write(&mut buf).unwrap(); - output[0].push(buf); -} - -/// Return boxed iterator of MIDI events -pub fn parse_midi_input <'a> (input: MidiIter<'a>) -> Box, &'a [u8])> + 'a> { - Box::new(input.map(|RawMidi { time, bytes }|( - time as usize, - LiveEvent::parse(bytes).unwrap(), - bytes - ))) -} - -/// Update notes_in array -pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { - match message { - MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } - MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, - _ => {} - } -} - -#[cfg(test)] #[test] pub fn test_midi_clip () { - let clip = MidiClip::stop_all(); - println!("{clip:?}"); - let clip = MidiClip::default(); - let mut clip = MidiClip::new("clip", true, 1, None, None); - clip.set_length(96); - clip.toggle_loop(); - clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); - assert!(clip.contains_note_on(36.into(), 6, 18)); - assert_eq!(&clip.notes, &clip.duplicate().notes); - let clip = std::sync::Arc::new(clip); - assert_eq!(clip.clone(), clip); -} -#[cfg(test)] #[test] pub fn test_midi_edit () { - let editor = MidiEditor::default(); - println!("{editor:?}"); -} -#[cfg(test)] #[test] pub fn test_midi_player () { - let player = MidiPlayer::default(); - println!("{player:?}"); -} +mod clip; pub use self::clip::*; +mod mode; pub use self::mode::*; +mod note; pub use self::note::*; +mod piano; pub use self::piano::*; +mod pool; pub use self::pool::*; +mod port; pub use self::port::*; diff --git a/crates/midi/src/midi_pool.rs b/crates/midi/src/midi_pool.rs deleted file mode 100644 index 7fabb7fa..00000000 --- a/crates/midi/src/midi_pool.rs +++ /dev/null @@ -1,561 +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() - } - } - } -} -#[derive(Debug)] -pub struct MidiPool { - pub visible: bool, - /// Collection of clips - pub clips: Arc>>>>, - /// Selected clip - pub clip: AtomicUsize, - /// Mode switch - pub mode: Option, - - keys: SourceIter<'static>, - keys_rename: SourceIter<'static>, - keys_length: SourceIter<'static>, - keys_file: SourceIter<'static>, -} -/// 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), -} -impl Default for MidiPool { - fn default () -> Self { - Self { - visible: true, - clips: Arc::from(RwLock::from(vec![])), - clip: 0.into(), - mode: None, - keys: SourceIter(KEYS_POOL), - keys_rename: SourceIter(KEYS_RENAME), - keys_length: SourceIter(KEYS_LENGTH), - keys_file: SourceIter(KEYS_FILE), - } - } -} -from!(|clip:&Arc>|MidiPool = { - let model = Self::default(); - model.clips.write().unwrap().push(clip.clone()); - model.clip.store(1, Relaxed); - model -}); -has_clips!(|self: MidiPool|self.clips); -has_clip!(|self: MidiPool|self.clips().get(self.clip_index()).map(|c|c.clone())); -impl MidiPool { - fn clip_index (&self) -> usize { self.clip.load(Relaxed) } - fn set_clip_index (&self, value: usize) { self.clip.store(value, Relaxed); } - fn mode (&self) -> &Option { &self.mode } - fn mode_mut (&mut self) -> &mut Option { &mut self.mode } - 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 - )); - } - 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 - )); - } - fn begin_import (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Import( - self.clip_index(), - FileBrowser::new(None)? - )); - Ok(()) - } - fn begin_export (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Export( - self.clip_index(), - FileBrowser::new(None)? - )); - Ok(()) - } -} -/// 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, } - } -} -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()), - } -}); -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) { - let _undo = command.execute(self)?; - Some(true) - } else { - None - }) -}); -provide!(bool: |self: MidiPool| {}); -impl MidiPool { - pub fn new_clip (&self) -> MidiClip { - MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemPalette::random())) - } - pub fn cloned_clip (&self) -> MidiClip { - let index = self.clip_index(); - let mut clip = self.clips()[index].read().unwrap().duplicate(); - clip.color = ItemPalette::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) - } -} -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 = ItemPalette::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!() -}); -#[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 -}); -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 -}); -/////////////////////////////////////////////////////////////////////////////////////////////////// -//fn to_clips_command (state: &MidiPool, input: &Event) -> Option { - //use KeyCode::{Up, Down, Delete, Char}; - //use PoolCommand as Cmd; - //let index = state.clip_index(); - //let count = state.clips().len(); - //Some(match input { - //kpat!(Char('n')) => Cmd::Rename(ClipRenameCommand::Begin), - //kpat!(Char('t')) => Cmd::Length(ClipLengthCommand::Begin), - //kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin), - //kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin), - //kpat!(Char('c')) => Cmd::Clip(PoolClipCommand::SetColor(index, ItemColor::random())), - //kpat!(Char('[')) | kpat!(Up) => Cmd::Select( - //index.overflowing_sub(1).0.min(state.clips().len() - 1) - //), - //kpat!(Char(']')) | kpat!(Down) => Cmd::Select( - //index.saturating_add(1) % state.clips().len() - //), - //kpat!(Char('<')) => if index > 1 { - //state.set_clip_index(state.clip_index().saturating_sub(1)); - //Cmd::Clip(PoolClipCommand::Swap(index - 1, index)) - //} else { - //return None - //}, - //kpat!(Char('>')) => if index < count.saturating_sub(1) { - //state.set_clip_index(state.clip_index() + 1); - //Cmd::Clip(PoolClipCommand::Swap(index + 1, index)) - //} else { - //return None - //}, - //kpat!(Delete) => if index > 0 { - //state.set_clip_index(index.min(count.saturating_sub(1))); - //Cmd::Clip(PoolClipCommand::Delete(index)) - //} else { - //return None - //}, - //kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(PoolClipCommand::Add(count, MidiClip::new( - //"Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) - //))), - //kpat!(Char('i')) => Cmd::Clip(PoolClipCommand::Add(index + 1, MidiClip::new( - //"Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) - //))), - //kpat!(Char('d')) | kpat!(Shift-Char('D')) => { - //let mut clip = state.clips()[index].read().unwrap().duplicate(); - //clip.color = ItemPalette::random_near(clip.color, 0.25); - //Cmd::Clip(PoolClipCommand::Add(index + 1, clip)) - //}, - //_ => return None - //}) -//} diff --git a/crates/midi/src/mode.rs b/crates/midi/src/mode.rs new file mode 100644 index 00000000..6a43eb54 --- /dev/null +++ b/crates/midi/src/mode.rs @@ -0,0 +1,18 @@ +use crate::*; + +mod mode_browse; pub use self::mode_browse::*; +mod mode_length; pub use self::mode_length::*; +mod mode_rename; pub use self::mode_rename::*; + +/// 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/midi/src/mode/mode_browse.rs b/crates/midi/src/mode/mode_browse.rs new file mode 100644 index 00000000..dbf344aa --- /dev/null +++ b/crates/midi/src/mode/mode_browse.rs @@ -0,0 +1,40 @@ +use crate::*; + +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 +}); diff --git a/crates/midi/src/mode/mode_length.rs b/crates/midi/src/mode/mode_length.rs new file mode 100644 index 00000000..28944fd7 --- /dev/null +++ b/crates/midi/src/mode/mode_length.rs @@ -0,0 +1,133 @@ +use crate::*; + +/// 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() + } +} + +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()), + } +}); + +/// 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, } + } +} + +#[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/midi/src/mode/mode_rename.rs b/crates/midi/src/mode/mode_rename.rs new file mode 100644 index 00000000..6e3d8844 --- /dev/null +++ b/crates/midi/src/mode/mode_rename.rs @@ -0,0 +1,38 @@ +use crate::*; + +#[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!() +}); diff --git a/crates/midi/src/note.rs b/crates/midi/src/note.rs new file mode 100644 index 00000000..e7e35888 --- /dev/null +++ b/crates/midi/src/note.rs @@ -0,0 +1,3 @@ +mod note_pitch; pub use self::note_pitch::*; +mod note_point; pub use self::note_point::*; +mod note_range; pub use self::note_range::*; diff --git a/crates/midi/src/midi_pitch.rs b/crates/midi/src/note/note_pitch.rs similarity index 100% rename from crates/midi/src/midi_pitch.rs rename to crates/midi/src/note/note_pitch.rs diff --git a/crates/midi/src/midi_point.rs b/crates/midi/src/note/note_point.rs similarity index 100% rename from crates/midi/src/midi_point.rs rename to crates/midi/src/note/note_point.rs diff --git a/crates/midi/src/midi_range.rs b/crates/midi/src/note/note_range.rs similarity index 100% rename from crates/midi/src/midi_range.rs rename to crates/midi/src/note/note_range.rs diff --git a/crates/midi/src/piano.rs b/crates/midi/src/piano.rs new file mode 100644 index 00000000..ea7b9152 --- /dev/null +++ b/crates/midi/src/piano.rs @@ -0,0 +1,2 @@ +mod piano_h; pub use self::piano_h::*; +mod piano_v; pub use self::piano_v::*; diff --git a/crates/midi/src/piano_h.rs b/crates/midi/src/piano/piano_h.rs similarity index 99% rename from crates/midi/src/piano_h.rs rename to crates/midi/src/piano/piano_h.rs index e879d758..c647da33 100644 --- a/crates/midi/src/piano_h.rs +++ b/crates/midi/src/piano/piano_h.rs @@ -1,5 +1,6 @@ use crate::*; use Color::*; + /// A clip, rendered as a horizontal piano roll. pub struct PianoHorizontal { pub clip: Option>>, @@ -16,6 +17,7 @@ pub struct PianoHorizontal { /// Width of the keyboard pub keys_width: u16, } + impl PianoHorizontal { pub fn new (clip: Option<&Arc>>) -> Self { let size = Measure::new(); @@ -35,9 +37,11 @@ impl PianoHorizontal { 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())), @@ -230,20 +234,24 @@ impl TimeRange for PianoHorizontal { 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) { self.point.set_note_len(x) } fn note_pos (&self) -> usize { self.point.note_pos() } fn set_note_pos (&self, x: 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) { self.point.set_time_pos(x) } } + impl MidiViewer for PianoHorizontal { fn clip (&self) -> &Option>> { &self.clip diff --git a/crates/midi/src/piano_v.rs b/crates/midi/src/piano/piano_v.rs similarity index 100% rename from crates/midi/src/piano_v.rs rename to crates/midi/src/piano/piano_v.rs diff --git a/crates/midi/src/pool.rs b/crates/midi/src/pool.rs new file mode 100644 index 00000000..a9964d05 --- /dev/null +++ b/crates/midi/src/pool.rs @@ -0,0 +1,4 @@ +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/midi/src/pool/pool_api.rs b/crates/midi/src/pool/pool_api.rs new file mode 100644 index 00000000..d7c7bda8 --- /dev/null +++ b/crates/midi/src/pool/pool_api.rs @@ -0,0 +1,177 @@ +use crate::*; + +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) { + 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 = ItemPalette::from(color); + std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color); + Some(Self::SetColor(index, color.base)) + }, + }) + } +} diff --git a/crates/midi/src/pool/pool_clips.rs b/crates/midi/src/pool/pool_clips.rs new file mode 100644 index 00000000..65a671aa --- /dev/null +++ b/crates/midi/src/pool/pool_clips.rs @@ -0,0 +1,26 @@ +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/midi/src/pool/pool_model.rs b/crates/midi/src/pool/pool_model.rs new file mode 100644 index 00000000..b238b1cf --- /dev/null +++ b/crates/midi/src/pool/pool_model.rs @@ -0,0 +1,106 @@ +use crate::*; + +#[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: SourceIter<'static>, + pub keys_rename: SourceIter<'static>, + pub keys_length: SourceIter<'static>, + pub keys_file: SourceIter<'static>, +} + +impl Default for MidiPool { + fn default () -> Self { + Self { + visible: true, + clips: Arc::from(RwLock::from(vec![])), + clip: 0.into(), + mode: None, + keys: SourceIter(include_str!("../../edn/keys_pool.edn")), + keys_file: SourceIter(include_str!("../../edn/keys_pool_file.edn")), + keys_rename: SourceIter(include_str!("../../edn/keys_clip_rename.edn")), + keys_length: SourceIter(include_str!("../../edn/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(ItemPalette::random())) + } + pub fn cloned_clip (&self) -> MidiClip { + let index = self.clip_index(); + let mut clip = self.clips()[index].read().unwrap().duplicate(); + clip.color = ItemPalette::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) + } +} diff --git a/crates/midi/src/pool/pool_view.rs b/crates/midi/src/pool/pool_view.rs new file mode 100644 index 00000000..9cfe8b10 --- /dev/null +++ b/crates/midi/src/pool/pool_view.rs @@ -0,0 +1,29 @@ +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), "◀"))))), + )))) + }))))) +}); diff --git a/crates/midi/src/port.rs b/crates/midi/src/port.rs new file mode 100644 index 00000000..25977123 --- /dev/null +++ b/crates/midi/src/port.rs @@ -0,0 +1,31 @@ +use crate::*; + +mod port_in; pub use self::port_in::*; +mod port_out; pub use self::port_out::*; + +/// Update notes_in array +pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { + match message { + MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } + MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, + _ => {} + } +} + +/// Return boxed iterator of MIDI events +pub fn parse_midi_input <'a> (input: MidiIter<'a>) -> Box, &'a [u8])> + 'a> { + Box::new(input.map(|RawMidi { time, bytes }|( + time as usize, + LiveEvent::parse(bytes).unwrap(), + bytes + ))) +} + +/// Add "all notes off" to the start of a buffer. +pub fn all_notes_off (output: &mut [Vec>]) { + let mut buf = vec![]; + let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; + let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; + evt.write(&mut buf).unwrap(); + output[0].push(buf); +} diff --git a/crates/midi/src/midi_in.rs b/crates/midi/src/port/port_in.rs similarity index 96% rename from crates/midi/src/midi_in.rs rename to crates/midi/src/port/port_in.rs index e8ab6aac..abbd1390 100644 --- a/crates/midi/src/midi_in.rs +++ b/crates/midi/src/port/port_in.rs @@ -3,25 +3,41 @@ use crate::*; /// Trait for thing that may receive MIDI. pub trait HasMidiIns { fn midi_ins (&self) -> &Vec; + fn midi_ins_mut (&mut self) -> &mut Vec; + fn has_midi_ins (&self) -> bool { !self.midi_ins().is_empty() } } pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns { - fn notes_in (&self) -> &Arc>; + fn notes_in (&self) -> &Arc>; fn recording (&self) -> bool; + fn recording_mut (&mut self) -> &mut bool; + fn toggle_record (&mut self) { *self.recording_mut() = !self.recording(); } + fn monitoring (&self) -> bool; + fn monitoring_mut (&mut self) -> &mut bool; + fn toggle_monitor (&mut self) { *self.monitoring_mut() = !self.monitoring(); } + + fn overdub (&self) -> bool; + + fn overdub_mut (&mut self) -> &mut bool; + + fn toggle_overdub (&mut self) { + *self.overdub_mut() = !self.overdub(); + } + fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { // For highlighting keys and note repeat let notes_in = self.notes_in().clone(); @@ -32,11 +48,13 @@ pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns { if monitoring { midi_buf[sample].push(bytes.to_vec()); } + // FIXME: don't lock on every event! update_keys(&mut notes_in.write().unwrap(), &message); } } } } + fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { if self.monitoring() { self.monitor(scope, midi_buf); @@ -51,6 +69,7 @@ pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns { self.record_next(); } } + fn record_clip ( &mut self, scope: &ProcessScope, @@ -80,12 +99,9 @@ pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns { } } } + fn record_next (&mut self) { // TODO switch to next clip and record into it } - fn overdub (&self) -> bool; - fn overdub_mut (&mut self) -> &mut bool; - fn toggle_overdub (&mut self) { - *self.overdub_mut() = !self.overdub(); - } + } diff --git a/crates/midi/src/midi_out.rs b/crates/midi/src/port/port_out.rs similarity index 100% rename from crates/midi/src/midi_out.rs rename to crates/midi/src/port/port_out.rs