use crate::*; use KeyCode::{Char, Up, Down, Left, Right, Enter}; use MidiEditCommand::*; pub trait HasEditor { fn editor (&self) -> &MidiEditor; } #[macro_export] macro_rules! has_editor { (|$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 phrase pub struct MidiEditor { pub mode: PianoHorizontal, pub size: Measure } from!(|phrase: &Arc>|MidiEditor = { let mut model = Self::from(Some(phrase.clone())); model.redraw(); model }); from!(|phrase: Option>>|MidiEditor = { let mut model = Self::default(); *model.phrase_mut() = phrase; model.redraw(); model }); impl Default for MidiEditor { fn default () -> Self { let mut mode = PianoHorizontal::new(None); mode.redraw(); Self { mode, size: Measure::new() } } } has_size!(|self: MidiEditor|&self.size); render!(TuiOut: (self: MidiEditor) => { self.autoscroll(); self.autozoom(); Fill::xy(Bsp::b(&self.size, &self.mode)) }); 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) { self.mode.set_note_len(x) } fn note_point (&self) -> usize { self.mode.note_point() } fn set_note_point (&self, x: usize) { self.mode.set_note_point(x) } } impl TimePoint for MidiEditor { fn time_point (&self) -> usize { self.mode.time_point() } fn set_time_point (&self, x: usize) { self.mode.set_time_point(x) } } impl MidiViewer for MidiEditor { fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize) { self.mode.buffer_size(phrase) } fn redraw (&self) { self.mode.redraw() } fn phrase (&self) -> &Option>> { self.mode.phrase() } fn phrase_mut (&mut self) -> &mut Option>> { self.mode.phrase_mut() } fn set_phrase (&mut self, phrase: Option<&Arc>>) { self.mode.set_phrase(phrase) } } impl MidiEditor { /// Put note at current position pub fn put_note (&mut self, advance: bool) { let mut redraw = false; if let Some(phrase) = self.phrase() { let mut phrase = phrase.write().unwrap(); let note_start = self.time_point(); let note_point = self.note_point(); let note_len = self.note_len(); let note_end = note_start + (note_len.saturating_sub(1)); let key: u7 = u7::from(note_point as u8); let vel: u7 = 100.into(); let length = phrase.length; let note_end = note_end % length; let note_on = MidiMessage::NoteOn { key, vel }; if !phrase.notes[note_start].iter().any(|msg|*msg == note_on) { phrase.notes[note_start].push(note_on); } let note_off = MidiMessage::NoteOff { key, vel }; if !phrase.notes[note_end].iter().any(|msg|*msg == note_off) { phrase.notes[note_end].push(note_off); } if advance { self.set_time_point(note_end); } redraw = true; } if redraw { self.mode.redraw(); } } } 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() } } #[derive(Clone, Debug)] pub enum MidiEditCommand { // TODO: 1-9 seek markers that by default start every 8th of the phrase AppendNote, PutNote, SetNoteCursor(usize), SetNoteLength(usize), SetNoteScroll(usize), SetTimeCursor(usize), SetTimeScroll(usize), SetTimeZoom(usize), SetTimeLock(bool), Show(Option>>), } handle!(TuiIn: |self: MidiEditor, input|MidiEditCommand::execute_with_state(self, input.event())); keymap!(KEYS_MIDI_EDITOR = |s: MidiEditor, _input: Event| MidiEditCommand { key(Up) => SetNoteCursor(s.note_point() + 1), key(Char('w')) => SetNoteCursor(s.note_point() + 1), key(Down) => SetNoteCursor(s.note_point().saturating_sub(1)), key(Char('s')) => SetNoteCursor(s.note_point().saturating_sub(1)), key(Left) => SetTimeCursor(s.time_point().saturating_sub(s.note_len())), key(Char('a')) => SetTimeCursor(s.time_point().saturating_sub(s.note_len())), key(Right) => SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length()), ctrl(alt(key(Up))) => SetNoteScroll(s.note_point() + 3), ctrl(alt(key(Down))) => SetNoteScroll(s.note_point().saturating_sub(3)), ctrl(alt(key(Left))) => SetTimeScroll(s.time_point().saturating_sub(s.time_zoom().get())), ctrl(alt(key(Right))) => SetTimeScroll((s.time_point() + s.time_zoom().get()) % s.phrase_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_point() + 3), alt(key(Down)) => SetNoteCursor(s.note_point().saturating_sub(3)), alt(key(Left)) => SetTimeCursor(s.time_point().saturating_sub(s.time_zoom().get())), alt(key(Right)) => SetTimeCursor((s.time_point() + s.time_zoom().get()) % s.phrase_length()), key(Char('d')) => SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length()), key(Char('z')) => SetTimeLock(!s.time_lock().get()), key(Char('-')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::next(s.time_zoom().get()) }), key(Char('_')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::next(s.time_zoom().get()) }), key(Char('=')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::prev(s.time_zoom().get()) }), key(Char('+')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::prev(s.time_zoom().get()) }), key(Enter) => PutNote, ctrl(key(Enter)) => AppendNote, key(Char(',')) => SetNoteLength(Note::prev(s.note_len())), key(Char('.')) => SetNoteLength(Note::next(s.note_len())), key(Char('<')) => SetNoteLength(Note::prev(s.note_len())), key(Char('>')) => SetNoteLength(Note::next(s.note_len())), //// TODO: kpat!(Char('/')) => // toggle 3plet //// TODO: kpat!(Char('?')) => // toggle dotted }); impl MidiEditor { fn phrase_length (&self) -> usize { self.phrase().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } } impl Command for MidiEditCommand { fn execute (self, state: &mut MidiEditor) -> Perhaps { use MidiEditCommand::*; match self { Show(phrase) => { state.set_phrase(phrase.as_ref()); }, 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) => { state.set_note_len(x); }, SetTimeCursor(x) => { state.set_time_point(x); }, SetNoteCursor(note) => { state.set_note_point(note.min(127)); }, _ => todo!("{:?}", self) } Ok(None) } }