//! MIDI editor. use crate::*; /// Contains state for viewing and editing a clip pub struct MidiEditor { pub mode: PianoHorizontal, pub size: Measure, pub 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(), ":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), }); 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) } } 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::<_, MidiEditCommand, _>(self, input) { let _undo = 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) } }