tek/src/midi/midi_editor.rs

236 lines
9.6 KiB
Rust

use crate::*;
use KeyCode::{Char, Up, Down, Left, Right, Enter};
use PhraseCommand::*;
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 }
}
}
}
#[derive(Clone, Debug)]
pub enum PhraseCommand {
// 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<Arc<RwLock<MidiClip>>>),
}
event_map_input_to_command!(Tui: MidiEditor: PhraseCommand: MidiEditor::KEYS);
pub(crate) type KeyMapping<const N: usize, E, T, U> = [(E, &'static dyn Fn(&T)->U);N];
impl MidiEditor {
const KEYS: KeyMapping<31, Event, Self, PhraseCommand> = [
(kexp!(Ctrl-Alt-Up), &|s: &Self|SetNoteScroll(s.note_point() + 3)),
(kexp!(Ctrl-Alt-Down), &|s: &Self|SetNoteScroll(s.note_point().saturating_sub(3))),
(kexp!(Ctrl-Alt-Left), &|s: &Self|SetTimeScroll(s.time_point().saturating_sub(s.time_zoom().get()))),
(kexp!(Ctrl-Alt-Right), &|s: &Self|SetTimeScroll((s.time_point() + s.time_zoom().get()) % s.phrase_length())),
(kexp!(Ctrl-Up), &|s: &Self|SetNoteScroll(s.note_lo().get() + 1)),
(kexp!(Ctrl-Down), &|s: &Self|SetNoteScroll(s.note_lo().get().saturating_sub(1))),
(kexp!(Ctrl-Left), &|s: &Self|SetTimeScroll(s.time_start().get().saturating_sub(s.note_len()))),
(kexp!(Ctrl-Right), &|s: &Self|SetTimeScroll(s.time_start().get() + s.note_len())),
(kexp!(Alt-Up), &|s: &Self|SetNoteCursor(s.note_point() + 3)),
(kexp!(Alt-Down), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(3))),
(kexp!(Alt-Left), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.time_zoom().get()))),
(kexp!(Alt-Right), &|s: &Self|SetTimeCursor((s.time_point() + s.time_zoom().get()) % s.phrase_length())),
(kexp!(Up), &|s: &Self|SetNoteCursor(s.note_point() + 1)),
(kexp!(Char('w')), &|s: &Self|SetNoteCursor(s.note_point() + 1)),
(kexp!(Down), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(1))),
(kexp!(Char('s')), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(1))),
(kexp!(Left), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.note_len()))),
(kexp!(Char('a')), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.note_len()))),
(kexp!(Right), &|s: &Self|SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length())),
(kexp!(Char('d')), &|s: &Self|SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length())),
(kexp!(Char('z')), &|s: &Self|SetTimeLock(!s.time_lock().get())),
(kexp!(Char('-')), &|s: &Self|SetTimeZoom(Note::next(s.time_zoom().get()))),
(kexp!(Char('_')), &|s: &Self|SetTimeZoom(Note::next(s.time_zoom().get()))),
(kexp!(Char('=')), &|s: &Self|SetTimeZoom(Note::prev(s.time_zoom().get()))),
(kexp!(Char('+')), &|s: &Self|SetTimeZoom(Note::prev(s.time_zoom().get()))),
(kexp!(Enter), &|s: &Self|PutNote),
(kexp!(Ctrl-Enter), &|s: &Self|AppendNote),
(kexp!(Char(',')), &|s: &Self|SetNoteLength(Note::prev(s.note_len()))), // TODO: no 3plet
(kexp!(Char('.')), &|s: &Self|SetNoteLength(Note::next(s.note_len()))),
(kexp!(Char('<')), &|s: &Self|SetNoteLength(Note::prev(s.note_len()))), // TODO: 3plet
(kexp!(Char('>')), &|s: &Self|SetNoteLength(Note::next(s.note_len()))),
//// TODO: key_pat!(Char('/')) => // toggle 3plet
//// TODO: key_pat!(Char('?')) => // toggle dotted
];
fn phrase_length (&self) -> usize {
self.phrase().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
}
}
impl Command<MidiEditor> for PhraseCommand {
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {
use PhraseCommand::*;
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)
}
}
/// Contains state for viewing and editing a phrase
pub struct MidiEditor {
/// Contents the phrase
pub mode: Box<dyn PhraseViewMode>,
pub size: Measure<Tui>
}
impl Default for MidiEditor {
fn default () -> Self {
let mut mode = Box::new(PianoHorizontal::new(None));
mode.redraw();
Self { mode, size: Measure::new() }
}
}
has_size!(<Tui>|self: MidiEditor|&self.size);
render!(Tui: (self: MidiEditor) => {
self.autoscroll();
self.autozoom();
&self.mode
});
//render!(<Tui>|self: MidiEditor|lay!(|add|{add(&self.size)?;add(self.mode)}));//bollocks
pub trait PhraseViewMode: HasSize<Tui> + MidiRange + MidiPoint + Debug + Send + Sync {
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize);
fn redraw (&mut self);
fn phrase (&self) -> &Option<Arc<RwLock<MidiClip>>>;
fn phrase_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>>;
fn set_phrase (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
*self.phrase_mut() = phrase.cloned();
self.redraw();
}
}
impl Content<Tui> for Box<dyn PhraseViewMode> {
fn content (&self) -> impl Content<Tui> {
Some(&(*self))
}
}
impl MidiView<Tui> for MidiEditor {}
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 PhraseViewMode for MidiEditor {
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize) {
self.mode.buffer_size(phrase)
}
fn redraw (&mut self) {
self.mode.redraw()
}
fn phrase (&self) -> &Option<Arc<RwLock<MidiClip>>> {
self.mode.phrase()
}
fn phrase_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
self.mode.phrase_mut()
}
fn set_phrase (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
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();
}
}
}
from!(|phrase: &Arc<RwLock<MidiClip>>|MidiEditor = {
let mut model = Self::from(Some(phrase.clone()));
model.redraw();
model
});
from!(|phrase: Option<Arc<RwLock<MidiClip>>>|MidiEditor = {
let mut model = Self::default();
*model.phrase_mut() = phrase;
model.redraw();
model
});
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()
}
}