diff --git a/src/arrange.rs b/src/arrange.rs index d93501e9..b8ac5062 100644 --- a/src/arrange.rs +++ b/src/arrange.rs @@ -1,8 +1,7 @@ use ::std::sync::{Arc, RwLock}; use ::tengri::{space::east, color::ItemTheme}; use ::tengri::{draw::*, term::*}; -use crate::{*, device::*, sequence::*, clock::*, select::*, sample::*}; -impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } +use crate::{*, device::*, editor::*, sequence::*, clock::*, select::*, sample::*}; /// Arranger. /// @@ -20,9 +19,9 @@ impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &sel /// TODO rename to "render_cache" or smth pub arranger: Arc>, /// Display size - pub size: [AtomicUsize; 2], + pub size: Size, /// Display size of clips area - pub size_inner: [AtomicUsize; 2], + pub size_inner: Size, /// Source of time #[cfg(feature = "clock")] pub clock: Clock, /// Allows one MIDI clip to be edited @@ -84,7 +83,11 @@ impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &sel } impl_has!(Jack<'static>: |self: Arrangement| self.jack); -impl_has!([AtomicUsize; 2]: |self: Arrangement| self.size); +impl HasJack<'static> for Arrangement { + fn jack (&self) -> &Jack<'static> { &self.jack } +} + +impl_has!(Size: |self: Arrangement| self.size); impl_has!(Vec: |self: Arrangement| self.tracks); impl_has!(Vec: |self: Arrangement| self.scenes); impl_has!(Vec: |self: Arrangement| self.midi_ins); @@ -110,67 +113,69 @@ pub trait ClipsView: TracksView + ScenesView { { self.clips_size().of(wh_full(above( wh_full(origin_se(fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), - thunk(|to: &mut Tui|for ( - track_index, track, _, _ - ) in self.tracks_with_sizes() { - to.place(&w_exact(track.width as u16, - h_full(self.view_track_clips(track_index, track)))) + thunk(|to: &mut Tui|{ + for (track_index, track, _, _) in self.tracks_with_sizes() { + to.place(&w_exact(track.width as u16, + h_full(self.view_track_clips(track_index, track)))) + } + Ok(XYWH(0, 0, 0, 0)) })))) } fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw + 'a { - thunk(move|to: &mut Tui|for ( - scene_index, scene, .. - ) in self.scenes_with_sizes() { - let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { - let clip = clip.read().unwrap(); - (format!(" ⏹ {}", &clip.name).into(), clip.color) - } else { - (" ⏹ -- ".into(), ItemTheme::G[32]) - }; - let fg = theme.lightest.term; - let mut outline = theme.base.term; - let bg = if self.selection().track() == Some(track_index) - && self.selection().scene() == Some(scene_index) - { - outline = theme.lighter.term; - theme.light.term - } else if self.selection().track() == Some(track_index) - || self.selection().scene() == Some(scene_index) - { - outline = theme.darkest.term; - theme.base.term - } else { - theme.dark.term - }; - let w = if self.selection().track() == Some(track_index) - && let Some(editor) = self.editor () - { - (editor.measure_width() as usize).max(24).max(track.width) - } else { - track.width - } as u16; - let y = if self.selection().scene() == Some(scene_index) - && let Some(editor) = self.editor () - { - (editor.measure_height() as usize).max(12) - } else { - Self::H_SCENE as usize - } as u16; + thunk(move|to: &mut Tui|{ + for (scene_index, scene, ..) in self.scenes_with_sizes() { + let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { + let clip = clip.read().unwrap(); + (format!(" ⏹ {}", &clip.name).into(), clip.color) + } else { + (" ⏹ -- ".into(), ItemTheme::G[32]) + }; + let fg = theme.lightest.term; + let mut outline = theme.base.term; + let bg = if self.selection().track() == Some(track_index) + && self.selection().scene() == Some(scene_index) + { + outline = theme.lighter.term; + theme.light.term + } else if self.selection().track() == Some(track_index) + || self.selection().scene() == Some(scene_index) + { + outline = theme.darkest.term; + theme.base.term + } else { + theme.dark.term + }; + let w = if self.selection().track() == Some(track_index) + && let Some(editor) = self.editor () + { + (editor.size.w() as usize).max(24).max(track.width) + } else { + track.width + } as u16; + let y = if self.selection().scene() == Some(scene_index) + && let Some(editor) = self.editor () + { + (editor.size.h() as usize).max(12) + } else { + Self::H_SCENE as usize + } as u16; - let is_selected = - self.selection().track() == Some(track_index) && - self.selection().scene() == Some(scene_index) && - self.is_editing(); + let is_selected = + self.selection().track() == Some(track_index) && + self.selection().scene() == Some(scene_index) && + self.is_editing(); - to.place(&wh_exact(w, y, below( - wh_full(Outer(true, Style::default().fg(outline))), - wh_full(below( - below( - fg_bg(outline, bg, wh_full("")), - wh_full(origin_nw(fg_bg(fg, bg, bold(true, name)))), - ), - wh_full(when(is_selected, self.editor().view()))))))); + to.place(&wh_exact(Some(w), Some(y), below( + wh_full(Outer(true, Style::default().fg(outline))), + wh_full(below( + below( + fg_bg(outline, bg, wh_full("")), + wh_full(origin_nw(fg_bg(fg, bg, bold(true, &name)))), + ), + wh_full(when(is_selected, self.editor().map(|e|e.view())))))))); + } + Ok(XYWH(0, 0, 0, 0)) }) } @@ -179,12 +184,12 @@ pub trait ClipsView: TracksView + ScenesView { pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { fn tracks_width_available (&self) -> u16 { - (self.measure_width() as u16).saturating_sub(40) + (self.size.width() as u16).saturating_sub(40) } /// Iterate over tracks with their corresponding sizes. fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { - let _editor_width = self.editor().map(|e|e.measure_width()); + let _editor_width = self.editor().map(|e|e.size.w()); let _active_track = self.selection().track(); let mut x = 0; self.tracks().iter().enumerate().map_while(move |(index, track)|{ @@ -222,23 +227,31 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { }, south(w_full(origin_nw(east( format!("·t{index:02} "), fg(Rgb(255, 255, 255), bold(true, &track.name)) - ))), ""))) ));}})))) + ))), ""))) ));} + Ok(XYWH(0, 0, 0, 0)) + })))) } fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw { view_track_row_section(theme, south(w_full(origin_w(button_2("o", "utput", false))), - thunk(|to: &mut Tui|for port in self.midi_outs().iter() { - to.place(&w_full(origin_w(port.port_name()))); + thunk(|to: &mut Tui|{ + for port in self.midi_outs().iter() { + to.place(&w_full(origin_w(port.port_name()))); + } + Ok(XYWH(0, 0, 0, 0)) })), button_2("O", "+", false), bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{ for (index, track, _x1, _x2) in self.tracks_with_sizes() { to.place(&w_exact(track_width(index, track), - origin_nw(h_full(iter_south(1, ||track.sequencer.midi_outs.iter(), + origin_nw(h_full(iter_south(||track.sequencer.midi_outs.iter(), |port, index|fg(Rgb(255, 255, 255), h_exact(1, bg(track.color.dark.term, w_full(origin_w( - format!("·o{index:02} {}", port.port_name())))))))))));}})))) + format!("·o{index:02} {}", port.port_name()))))))))))); + } + Ok(XYWH(0, 0, 0, 0)) + })))) } fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw { @@ -246,18 +259,21 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { for track in self.tracks().iter() { h = h.max(track.sequencer.midi_ins.len() as u16); } - let content = thunk(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&wh_exact(track_width(index, track), h + 1, - origin_nw(south( - bg(track.color.base.term, - w_full(origin_w(east!( - either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), - either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), - either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), - )))), - iter_south(1, ||track.sequencer.midi_ins.iter(), - |port, index|fg_bg(Rgb(255, 255, 255), track.color.dark.term, - w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))); + let content = thunk(move|to: &mut Tui|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&wh_exact(track_width(index, track), h + 1, + origin_nw(south( + bg(track.color.base.term, + w_full(origin_w(east!( + either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), + either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), + either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), + )))), + iter_south(1, ||track.sequencer.midi_ins.iter(), + |port, index|fg_bg(Rgb(255, 255, 255), track.color.dark.term, + w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))); + } + Ok(XYWH(0, 0, 0, 0)) }); view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), bg(theme.darker.term, origin_w(content))) @@ -522,9 +538,9 @@ pub trait HasTrack: AsRefOpt + AsMutOpt { impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } impl ScenesView for App { - fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(self.w_side()) } + fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(self.w_side()) } fn w_side (&self) -> u16 { 20 } - fn h_scenes (&self) -> u16 { (self.measure_height() as u16).saturating_sub(20) } + fn h_scenes (&self) -> u16 { (self.size.h() as u16).saturating_sub(20) } } impl Scene { /// Returns the pulse length of the longest clip in the scene @@ -780,17 +796,17 @@ impl Arrangement { } impl ScenesView for Arrangement { fn h_scenes (&self) -> u16 { - (self.measure_height() as u16).saturating_sub(20) + (self.size.h() as u16).saturating_sub(20) } fn w_side (&self) -> u16 { - (self.measure_width() as u16 * 2 / 10).max(20) + (self.size.w() as u16 * 2 / 10).max(20) } fn w_mid (&self) -> u16 { - (self.measure_width() as u16).saturating_sub(2 * self.w_side()).max(40) + (self.size.width() as u16).saturating_sub(2 * self.w_side()).max(40) } } impl HasClipsSize for Arrangement { - fn clips_size (&self) -> &[AtomicUsize; 2] { &self.size_inner } + fn clips_size (&self) -> &Size { &self.size_inner } } pub type SceneWith<'a, T> = diff --git a/src/browse.rs b/src/browse.rs index 83d02a2e..71ae746c 100644 --- a/src/browse.rs +++ b/src/browse.rs @@ -21,7 +21,7 @@ def_command!(FileBrowserCommand: |sampler: Sampler|{ pub filter: String, pub index: usize, pub scroll: usize, - pub size: [AtomicUsize; 2], + pub size: Size, } pub(crate) struct EntriesIterator<'a> { @@ -330,7 +330,7 @@ impl Browse { } } impl<'a> Iterator for EntriesIterator<'a> { - type Item = Modify<&'a str>; + type Item = (); fn next (&mut self) -> Option { let dirs = self.browser.dirs.len(); let files = self.browser.files.len(); diff --git a/src/editor.rs b/src/editor.rs new file mode 100644 index 00000000..82187504 --- /dev/null +++ b/src/editor.rs @@ -0,0 +1,818 @@ +use crate::*; + +/// Contains state for viewing and editing a clip. +/// +/// ``` +/// use std::sync::{Arc, RwLock}; +/// let clip = tek::MidiClip::stop_all(); +/// let mut editor = tek::MidiEditor { +/// mode: tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip)))), +/// size: Default::default(), +/// //keys: Default::default(), +/// }; +/// let _ = editor.put_note(true); +/// let _ = editor.put_note(false); +/// let _ = editor.clip_status(); +/// let _ = editor.edit_status(); +/// ``` +pub struct MidiEditor { + /// Size of editor on screen + pub size: Size, + /// View mode and state of editor + pub mode: PianoHorizontal, +} + +/// A clip, rendered as a horizontal piano roll. +/// +/// ``` +/// let piano = tek::PianoHorizontal::default(); +/// ``` +#[derive(Clone, Default)] +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: Size, + /// The display window + pub range: MidiSelection, + /// The note cursor + pub point: MidiCursor, + /// The highlight color palette + pub color: ItemTheme, + /// Width of the keyboard + pub keys_width: u16, +} + +/// 12 piano keys, some highlighted. +/// +/// ``` +/// let keys = tek::OctaveVertical::default(); +/// ``` +#[derive(Copy, Clone)] +pub struct OctaveVertical { + pub on: [bool; 12], + pub colors: [Color; 3] +} + +/// +/// ``` +/// let _ = tek::MidiCursor::default(); +/// ``` +#[derive(Debug, Clone)] pub struct MidiCursor { + /// Time coordinate of cursor + pub time_pos: Arc, + /// Note coordinate of cursor + pub note_pos: Arc, + /// Length of note that will be inserted, in pulses + pub note_len: Arc, +} + +/// +/// ``` +/// use tek::{TimeRange, NoteRange}; +/// let model = tek::MidiSelection::from((1, false)); +/// +/// let _ = model.get_time_len(); +/// let _ = model.get_time_zoom(); +/// let _ = model.get_time_lock(); +/// let _ = model.get_time_start(); +/// let _ = model.get_time_axis(); +/// let _ = model.get_time_end(); +/// +/// let _ = model.get_note_lo(); +/// let _ = model.get_note_axis(); +/// let _ = model.get_note_hi(); +/// ``` +#[derive(Debug, Clone, Default)] pub struct MidiSelection { + pub time_len: Arc, + /// Length of visible time axis + pub time_axis: Arc, + /// Earliest time displayed + pub time_start: Arc, + /// Time step + pub time_zoom: Arc, + /// Auto rezoom to fit in time axis + pub time_lock: Arc, + /// Length of visible note axis + pub note_axis: Arc, + // Lowest note displayed + pub note_lo: Arc, +} + +/// ``` +/// use tek::{*, tengri::*}; +/// +/// struct Test(Option); +/// impl_as_ref_opt!(MidiEditor: |self: Test|self.0.as_ref()); +/// impl_as_mut_opt!(MidiEditor: |self: Test|self.0.as_mut()); +/// +/// let mut host = Test(Some(MidiEditor::default())); +/// let _ = host.editor(); +/// let _ = host.editor_mut(); +/// let _ = host.is_editing(); +/// let _ = host.editor_w(); +/// let _ = host.editor_h(); +/// ``` +pub trait HasEditor: AsRefOpt + AsMutOpt { + fn editor (&self) -> Option<&MidiEditor> { self.as_ref_opt() } + fn editor_mut (&mut self) -> Option<&mut MidiEditor> { self.as_mut_opt() } + fn is_editing (&self) -> bool { self.editor().is_some() } + fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) as usize } + fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) as usize } +} + +impl MidiPoint for T {} + +impl MidiRange for T {} + +impl +AsMutOpt> HasEditor for T {} + +def_command!(MidiEditCommand: |editor: MidiEditor| { + Show { clip: Option>> } => { + editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) }, + DeleteNote => { + editor.redraw(); todo!() }, + AppendNote { advance: bool } => { + editor.put_note(*advance); editor.redraw(); Ok(None) }, + SetNotePos { pos: usize } => { + editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) }, + SetNoteLen { len: usize } => { + editor.set_note_len(*len); editor.redraw(); Ok(None) }, + SetNoteScroll { scroll: usize } => { + editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) }, + SetTimePos { pos: usize } => { + editor.set_time_pos(*pos); editor.redraw(); Ok(None) }, + SetTimeScroll { scroll: usize } => { + editor.set_time_start(*scroll); editor.redraw(); Ok(None) }, + SetTimeZoom { zoom: usize } => { + editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) }, + SetTimeLock { lock: bool } => { + editor.set_time_lock(*lock); editor.redraw(); Ok(None) }, + // TODO: 1-9 seek markers that by default start every 8th of the clip +}); + +pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize); + fn redraw (&self); + fn clip (&self) -> &Option>>; + fn clip_mut (&mut self) -> &mut Option>>; + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.redraw(); + } + /// Make sure cursor is within note range + fn autoscroll (&self) { + let note_pos = self.get_note_pos().min(127); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + if note_pos < note_lo { + self.note_lo().set(note_pos); + } else if note_pos > note_hi { + self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi)); + } + } + /// Make sure time range is within display + fn autozoom (&self) { + if self.time_lock().get() { + let time_len = self.get_time_len(); + let time_axis = self.get_time_axis(); + let time_zoom = self.get_time_zoom(); + loop { + let time_zoom = self.time_zoom().get(); + let time_area = time_axis * time_zoom; + if time_area > time_len { + let next_time_zoom = note_duration_prev(time_zoom); + if next_time_zoom <= 1 { + break + } + let next_time_area = time_axis * next_time_zoom; + if next_time_area >= time_len { + self.time_zoom().set(next_time_zoom); + } else { + break + } + } else if time_area < time_len { + let prev_time_zoom = note_duration_next(time_zoom); + if prev_time_zoom > 384 { + break + } + let prev_time_area = time_axis * prev_time_zoom; + if prev_time_area <= time_len { + self.time_zoom().set(prev_time_zoom); + } else { + break + } + } + } + if time_zoom != self.time_zoom().get() { + self.redraw() + } + } + //while time_len.div_ceil(time_zoom) > time_axis { + //println!("\r{time_len} {time_zoom} {time_axis}"); + //time_zoom = Note::next(time_zoom); + //} + //self.time_zoom().set(time_zoom); + } +} + +pub trait NotePoint { + fn note_len (&self) -> &AtomicUsize; + /// Get the current length of the note cursor. + fn get_note_len (&self) -> usize { + self.note_len().load(Relaxed) + } + /// Set the length of the note cursor, returning the previous value. + fn set_note_len (&self, x: usize) -> usize { + self.note_len().swap(x, Relaxed) + } + + fn note_pos (&self) -> &AtomicUsize; + /// Get the current pitch of the note cursor. + fn get_note_pos (&self) -> usize { + self.note_pos().load(Relaxed).min(127) + } + /// Set the current pitch fo the note cursor, returning the previous value. + fn set_note_pos (&self, x: usize) -> usize { + self.note_pos().swap(x.min(127), Relaxed) + } +} + +pub trait TimePoint { + fn time_pos (&self) -> &AtomicUsize; + /// Get the current time position of the note cursor. + fn get_time_pos (&self) -> usize { + self.time_pos().load(Relaxed) + } + /// Set the current time position of the note cursor, returning the previous value. + fn set_time_pos (&self, x: usize) -> usize { + self.time_pos().swap(x, Relaxed) + } +} + +pub trait MidiPoint: NotePoint + TimePoint { + /// Get the current end of the note cursor. + fn get_note_end (&self) -> usize { + self.get_time_pos() + self.get_note_len() + } +} + +pub trait TimeRange { + fn time_len (&self) -> &AtomicUsize; + fn get_time_len (&self) -> usize { + self.time_len().load(Relaxed) + } + fn time_zoom (&self) -> &AtomicUsize; + fn get_time_zoom (&self) -> usize { + self.time_zoom().load(Relaxed) + } + fn set_time_zoom (&self, value: usize) -> usize { + self.time_zoom().swap(value, Relaxed) + } + fn time_lock (&self) -> &AtomicBool; + fn get_time_lock (&self) -> bool { + self.time_lock().load(Relaxed) + } + fn set_time_lock (&self, value: bool) -> bool { + self.time_lock().swap(value, Relaxed) + } + fn time_start (&self) -> &AtomicUsize; + fn get_time_start (&self) -> usize { + self.time_start().load(Relaxed) + } + fn set_time_start (&self, value: usize) -> usize { + self.time_start().swap(value, Relaxed) + } + fn time_axis (&self) -> &AtomicUsize; + fn get_time_axis (&self) -> usize { + self.time_axis().load(Relaxed) + } + fn get_time_end (&self) -> usize { + self.time_start().get() + self.time_axis().get() * self.time_zoom().get() + } +} + +pub trait NoteRange { + fn note_lo (&self) -> &AtomicUsize; + fn note_axis (&self) -> &AtomicUsize; + + fn get_note_lo (&self) -> usize { + self.note_lo().load(Relaxed) + } + fn set_note_lo (&self, x: usize) -> usize { + self.note_lo().swap(x, Relaxed) + } + fn get_note_axis (&self) -> usize { + self.note_axis().load(Relaxed) + } + fn get_note_hi (&self) -> usize { + (self.get_note_lo() + self.get_note_axis().saturating_sub(1)).min(127) + } +} + +pub trait MidiRange: TimeRange + NoteRange {} + +impl_has!(Size: |self: MidiEditor| self.size); +impl_has!(Size: |self: PianoHorizontal| self.size); + +impl Draw for MidiEditor { + fn draw(self, to: &mut Tui) -> Usually> { + self.tui().draw(to) + } +} +impl Draw for PianoHorizontal { + fn draw(self, to: &mut Tui) -> Usually> { + self.tui().draw(to) + } +} +impl std::fmt::Debug for MidiEditor { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("MidiEditor").field("mode", &self.mode).finish() + } +} +impl_from!(MidiEditor: |clip: &Arc>| { + let model = Self::from(Some(clip.clone())); + model.redraw(); + model +}); +impl_from!(MidiEditor: |clip: Option>>| { + let mut model = Self::default(); + *model.clip_mut() = clip; + model.redraw(); + model +}); +impl_default!(MidiEditor: Self { + size: [0, 0].into(), mode: PianoHorizontal::new(None) +}); +impl_default!(OctaveVertical: Self { + on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] +}); +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.get_time_pos(); + let note_pos = self.get_note_pos(); + let note_len = self.get_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 + 1) % clip.length); + } + redraw = true; + } + if redraw { + self.mode.redraw(); + } + } + fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } + fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } + fn note_length (&self) -> usize { self.get_note_len() } + fn note_pos (&self) -> usize { self.get_note_pos() } + fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } + fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } + fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } + fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } + fn note_len (&self) -> usize { self.get_note_len() } + fn note_len_next (&self) -> usize { self.get_note_len() + 1 } + fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } + fn note_range (&self) -> usize { self.get_note_axis() } + fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } + fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } + fn time_zoom (&self) -> usize { self.get_time_zoom() } + fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_lock (&self) -> bool { self.get_time_lock() } + fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } + fn time_pos (&self) -> usize { self.get_time_pos() } + fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } + fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } + fn time_pos_prev (&self) -> usize { + let step = self.get_note_len(); + self.get_time_pos().overflowing_sub(step) + .0.min(self.clip_length().saturating_sub(step)) + } + fn time_pos_prev_fine (&self) -> usize { + self.get_time_pos().overflowing_sub(1) + .0.min(self.clip_length().saturating_sub(1)) + } + pub fn clip_status (&self) -> impl Draw + '_ { + 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) }; + w_exact(20, south!( + w_full(origin_w(east( + button_2("f2", "name ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), + w_full(origin_w(east( + button_2("l", "ength ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), + w_full(origin_w(east( + button_2("r", "epeat ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), + )) + } + pub fn edit_status (&self) -> impl Draw + '_ { + 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.get_time_pos(); + let time_zoom = self.get_time_zoom(); + let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; + let note_pos = self.get_note_pos(); + let note_name = format!("{:4}", note_pitch_to_name(note_pos)); + let note_pos = format!("{:>3}", note_pos); + let note_len = format!("{:>4}", self.get_note_len()); + w_exact(20, south!( + w_full(origin_w(east( + button_2("t", "ime ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), + format!("{length} /{time_zoom} +{time_pos} "))))))), + w_full(origin_w(east( + button_2("z", "lock ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), + format!("{time_lock}"))))))), + w_full(origin_w(east( + button_2("x", "note ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), + 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) -> &AtomicUsize { self.mode.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } +} + +impl TimePoint for MidiEditor { + fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } +} + +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) } +} + +impl View for MidiEditor { + fn view (&self) -> impl Draw { + self.autoscroll(); + /*self.autozoom();*/ + self.size.of(&self.mode) + } +} + + +impl PianoHorizontal { + pub fn new (clip: Option<&Arc>>) -> Self { + let size = [0, 0].into(); + let mut range = MidiSelection::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: MidiCursor::default(), + clip: clip.cloned(), + color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), + }; + piano.redraw(); + piano + } +} + +impl PianoHorizontal { + fn tui (&self) -> impl Draw { + south( + east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), + east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(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, note_point: usize, time_point: 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(); + if note == (127-note_point) || time == time_point { + cell.set_bg(Rgb(0,0,0)); + } else { + cell.set_bg(clip.color.darkest.term); + } + if time % 384 == 0 { + cell.set_fg(clip.color.darker.term); + cell.set_char('│'); + } else if time % 96 == 0 { + cell.set_fg(clip.color.dark.term); + cell.set_char('╎'); + } else if time % note_len == 0 { + cell.set_fg(clip.color.darker.term); + cell.set_char('┊'); + } else if (127 - note) % 12 == 0 { + cell.set_fg(clip.color.darker.term); + cell.set_char('='); + } else if (127 - note) % 6 == 0 { + cell.set_fg(clip.color.darker.term); + cell.set_char('—'); + } else { + cell.set_fg(clip.color.darker.term); + 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.term);//.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 Draw { + let time_start = self.get_time_start(); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + let buffer = self.buffer.clone(); + Thunk::new(move|to: &mut Tui|{ + let source = buffer.read().unwrap(); + let XYWH(x0, y0, w, _h) = to.area(); + //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 Draw { + let note_hi = self.get_note_hi(); + let note_lo = self.get_note_lo(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let time_pos = self.get_time_pos(); + let time_start = self.get_time_start(); + let time_zoom = self.get_time_zoom(); + let style = Some(Style::default().fg(self.color.lightest.term)); + Thunk::new(move|to: &mut Tui|{ + let XYWH(x0, y0, w, _) = to.area(); + 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 Draw { + let state = self; + let color = state.color; + let note_lo = state.get_note_lo(); + let note_hi = state.get_note_hi(); + let note_pos = state.get_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.term).bold()); + h_full(w_exact(self.keys_width, Thunk::new(move|to: &mut Tui|{ + let XYWH(x, y0, _w, _h) = to.area(); + 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(¬e_pitch_to_name(note), x, screen_y, off_style) + }; + } + }))) + } + fn timeline (&self) -> impl Draw + '_ { + w_full(h_exact(1, Thunk::new(move|to: &mut Tui|{ + let XYWH(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); + } + } + }))) + } +} + +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) -> &AtomicUsize { self.point.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } +} + +impl TimePoint for PianoHorizontal { + fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } +} + +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 time_zoom = self.get_time_zoom(); + self.time_len().set(clip.length); + PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); + 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 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() + } +} +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 OctaveVertical { + fn tui (&self) -> impl Draw { + east!( + 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), "▟"), + ) + } +} +impl_from!(MidiSelection: |data:(usize, bool)| Self { + time_len: Arc::new(0.into()), + note_axis: Arc::new(0.into()), + note_lo: Arc::new(0.into()), + time_axis: Arc::new(0.into()), + time_start: Arc::new(0.into()), + time_zoom: Arc::new(data.0.into()), + time_lock: Arc::new(data.1.into()), +}); +impl_default!(MidiCursor: Self { + time_pos: Arc::new(0.into()), + note_pos: Arc::new(36.into()), + note_len: Arc::new(24.into()), +}); + +impl NotePoint for MidiCursor { + fn note_len (&self) -> &AtomicUsize { + &self.note_len + } + fn note_pos (&self) -> &AtomicUsize { + &self.note_pos + } +} + +impl TimePoint for MidiCursor { + fn time_pos (&self) -> &AtomicUsize { + self.time_pos.as_ref() + } +} + +impl TimeRange for MidiSelection { + fn time_len (&self) -> &AtomicUsize { &self.time_len } + fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } + fn time_lock (&self) -> &AtomicBool { &self.time_lock } + fn time_start (&self) -> &AtomicUsize { &self.time_start } + fn time_axis (&self) -> &AtomicUsize { &self.time_axis } +} + +impl NoteRange for MidiSelection { + fn note_lo (&self) -> &AtomicUsize { &self.note_lo } + fn note_axis (&self) -> &AtomicUsize { &self.note_axis } +} diff --git a/src/sample.rs b/src/sample.rs index 01dfbbe0..09b238ff 100644 --- a/src/sample.rs +++ b/src/sample.rs @@ -78,7 +78,7 @@ def_command!(SamplerCommand: |sampler: Sampler| { /// Currently active modal, if any. pub mode: Option, /// Size of rendered sampler. - pub size: [AtomicUsize;2], + pub size: Size, /// Lowest note displayed. pub note_lo: AtomicUsize, /// Currently selected note. diff --git a/src/sequence.rs b/src/sequence.rs index 11d852d3..fe15f60f 100644 --- a/src/sequence.rs +++ b/src/sequence.rs @@ -1,85 +1,7 @@ use crate::{*, clock::*, device::*}; -impl +AsMut> HasSequencer for T {} -impl +AsMutOpt> HasEditor for T {} -impl MidiPoint for T {} -impl MidiRange for T {} -def_command!(MidiEditCommand: |editor: MidiEditor| { - Show { clip: Option>> } => { - editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) }, - DeleteNote => { - editor.redraw(); todo!() }, - AppendNote { advance: bool } => { - editor.put_note(*advance); editor.redraw(); Ok(None) }, - SetNotePos { pos: usize } => { - editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) }, - SetNoteLen { len: usize } => { - editor.set_note_len(*len); editor.redraw(); Ok(None) }, - SetNoteScroll { scroll: usize } => { - editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) }, - SetTimePos { pos: usize } => { - editor.set_time_pos(*pos); editor.redraw(); Ok(None) }, - SetTimeScroll { scroll: usize } => { - editor.set_time_start(*scroll); editor.redraw(); Ok(None) }, - SetTimeZoom { zoom: usize } => { - editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) }, - SetTimeLock { lock: bool } => { - editor.set_time_lock(*lock); editor.redraw(); Ok(None) }, - // TODO: 1-9 seek markers that by default start every 8th of the clip -}); +impl +AsMut> HasSequencer for T {} -/// Contains state for viewing and editing a clip. -/// -/// ``` -/// use std::sync::{Arc, RwLock}; -/// let clip = tek::MidiClip::stop_all(); -/// let mut editor = tek::MidiEditor { -/// mode: tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip)))), -/// size: Default::default(), -/// //keys: Default::default(), -/// }; -/// let _ = editor.put_note(true); -/// let _ = editor.put_note(false); -/// let _ = editor.clip_status(); -/// let _ = editor.edit_status(); -/// ``` -pub struct MidiEditor { - /// Size of editor on screen - pub size: [AtomicUsize; 2], - /// View mode and state of editor - pub mode: PianoHorizontal, -} - -/// A clip, rendered as a horizontal piano roll. -/// -/// ``` -/// let piano = tek::PianoHorizontal::default(); -/// ``` -#[derive(Clone, Default)] 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: [AtomicUsize; 2], - /// The display window - pub range: MidiSelection, - /// The note cursor - pub point: MidiCursor, - /// The highlight color palette - pub color: ItemTheme, - /// Width of the keyboard - pub keys_width: u16, -} - -/// 12 piano keys, some highlighted. -/// -/// ``` -/// let keys = tek::OctaveVertical::default(); -/// ``` -#[derive(Copy, Clone)] pub struct OctaveVertical { - pub on: [bool; 12], - pub colors: [Color; 3] -} /// A MIDI sequence. /// /// ``` @@ -160,17 +82,11 @@ pub struct Sequencer { 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 { @@ -307,76 +223,9 @@ pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { } } -pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize); - fn redraw (&self); - fn clip (&self) -> &Option>>; - fn clip_mut (&mut self) -> &mut Option>>; - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.redraw(); - } - /// Make sure cursor is within note range - fn autoscroll (&self) { - let note_pos = self.get_note_pos().min(127); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - if note_pos < note_lo { - self.note_lo().set(note_pos); - } else if note_pos > note_hi { - self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi)); - } - } - /// Make sure time range is within display - fn autozoom (&self) { - if self.time_lock().get() { - let time_len = self.get_time_len(); - let time_axis = self.get_time_axis(); - let time_zoom = self.get_time_zoom(); - loop { - let time_zoom = self.time_zoom().get(); - let time_area = time_axis * time_zoom; - if time_area > time_len { - let next_time_zoom = note_duration_prev(time_zoom); - if next_time_zoom <= 1 { - break - } - let next_time_area = time_axis * next_time_zoom; - if next_time_area >= time_len { - self.time_zoom().set(next_time_zoom); - } else { - break - } - } else if time_area < time_len { - let prev_time_zoom = note_duration_next(time_zoom); - if prev_time_zoom > 384 { - break - } - let prev_time_area = time_axis * prev_time_zoom; - if prev_time_area <= time_len { - self.time_zoom().set(prev_time_zoom); - } else { - break - } - } - } - if time_zoom != self.time_zoom().get() { - self.redraw() - } - } - //while time_len.div_ceil(time_zoom) > time_axis { - //println!("\r{time_len} {time_zoom} {time_axis}"); - //time_zoom = Note::next(time_zoom); - //} - //self.time_zoom().set(time_zoom); - } -} -pub type MidiData = - Vec>; -pub type ClipPool = - Vec>>; -pub type CollectedMidiInput<'a> = - Vec, MidiError>)>>; +pub type MidiData = Vec>; +pub type ClipPool = Vec>>; +pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; pub trait HasClips { fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; @@ -387,180 +236,21 @@ pub trait HasClips { (self.clips().len() - 1, clip) } } -/// ``` -/// use tek::{*, tengri::*}; -/// -/// struct Test(Option); -/// impl_as_ref_opt!(MidiEditor: |self: Test|self.0.as_ref()); -/// impl_as_mut_opt!(MidiEditor: |self: Test|self.0.as_mut()); -/// -/// let mut host = Test(Some(MidiEditor::default())); -/// let _ = host.editor(); -/// let _ = host.editor_mut(); -/// let _ = host.is_editing(); -/// let _ = host.editor_w(); -/// let _ = host.editor_h(); -/// ``` -pub trait HasEditor: AsRefOpt + AsMutOpt { - fn editor (&self) -> Option<&MidiEditor> { self.as_ref_opt() } - fn editor_mut (&mut self) -> Option<&mut MidiEditor> { self.as_mut_opt() } - fn is_editing (&self) -> bool { self.editor().is_some() } - fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) as usize } - fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) as usize } -} + pub trait HasMidiClip { fn clip (&self) -> Option>>; } + pub trait HasSequencer: AsRef + AsMut { fn sequencer_mut (&mut self) -> &mut Sequencer { self.as_mut() } fn sequencer (&self) -> &Sequencer { self.as_ref() } } + pub trait HasMidiBuffers { fn note_buf_mut (&mut self) -> &mut Vec; fn midi_buf_mut (&mut self) -> &mut Vec>>; } - -pub trait NotePoint { - fn note_len (&self) -> &AtomicUsize; - /// Get the current length of the note cursor. - fn get_note_len (&self) -> usize { - self.note_len().load(Relaxed) - } - /// Set the length of the note cursor, returning the previous value. - fn set_note_len (&self, x: usize) -> usize { - self.note_len().swap(x, Relaxed) - } - - fn note_pos (&self) -> &AtomicUsize; - /// Get the current pitch of the note cursor. - fn get_note_pos (&self) -> usize { - self.note_pos().load(Relaxed).min(127) - } - /// Set the current pitch fo the note cursor, returning the previous value. - fn set_note_pos (&self, x: usize) -> usize { - self.note_pos().swap(x.min(127), Relaxed) - } -} - -pub trait TimePoint { - fn time_pos (&self) -> &AtomicUsize; - /// Get the current time position of the note cursor. - fn get_time_pos (&self) -> usize { - self.time_pos().load(Relaxed) - } - /// Set the current time position of the note cursor, returning the previous value. - fn set_time_pos (&self, x: usize) -> usize { - self.time_pos().swap(x, Relaxed) - } -} - -pub trait MidiPoint: NotePoint + TimePoint { - /// Get the current end of the note cursor. - fn get_note_end (&self) -> usize { - self.get_time_pos() + self.get_note_len() - } -} - -pub trait TimeRange { - fn time_len (&self) -> &AtomicUsize; - fn get_time_len (&self) -> usize { - self.time_len().load(Relaxed) - } - fn time_zoom (&self) -> &AtomicUsize; - fn get_time_zoom (&self) -> usize { - self.time_zoom().load(Relaxed) - } - fn set_time_zoom (&self, value: usize) -> usize { - self.time_zoom().swap(value, Relaxed) - } - fn time_lock (&self) -> &AtomicBool; - fn get_time_lock (&self) -> bool { - self.time_lock().load(Relaxed) - } - fn set_time_lock (&self, value: bool) -> bool { - self.time_lock().swap(value, Relaxed) - } - fn time_start (&self) -> &AtomicUsize; - fn get_time_start (&self) -> usize { - self.time_start().load(Relaxed) - } - fn set_time_start (&self, value: usize) -> usize { - self.time_start().swap(value, Relaxed) - } - fn time_axis (&self) -> &AtomicUsize; - fn get_time_axis (&self) -> usize { - self.time_axis().load(Relaxed) - } - fn get_time_end (&self) -> usize { - self.time_start().get() + self.time_axis().get() * self.time_zoom().get() - } -} - -pub trait NoteRange { - fn note_lo (&self) -> &AtomicUsize; - fn get_note_lo (&self) -> usize { - self.note_lo().load(Relaxed) - } - fn set_note_lo (&self, x: usize) -> usize { - self.note_lo().swap(x, Relaxed) - } - fn note_axis (&self) -> &AtomicUsize; - fn get_note_axis (&self) -> usize { - self.note_axis().load(Relaxed) - } - fn get_note_hi (&self) -> usize { - (self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127) - } -} - -pub trait MidiRange: TimeRange + NoteRange {} - -/// -/// ``` -/// let _ = tek::MidiCursor::default(); -/// ``` -#[derive(Debug, Clone)] pub struct MidiCursor { - /// Time coordinate of cursor - pub time_pos: Arc, - /// Note coordinate of cursor - pub note_pos: Arc, - /// Length of note that will be inserted, in pulses - pub note_len: Arc, -} - -/// -/// ``` -/// use tek::{TimeRange, NoteRange}; -/// let model = tek::MidiSelection::from((1, false)); -/// -/// let _ = model.get_time_len(); -/// let _ = model.get_time_zoom(); -/// let _ = model.get_time_lock(); -/// let _ = model.get_time_start(); -/// let _ = model.get_time_axis(); -/// let _ = model.get_time_end(); -/// -/// let _ = model.get_note_lo(); -/// let _ = model.get_note_axis(); -/// let _ = model.get_note_hi(); -/// ``` -#[derive(Debug, Clone, Default)] pub struct MidiSelection { - pub time_len: Arc, - /// Length of visible time axis - pub time_axis: Arc, - /// Earliest time displayed - pub time_start: Arc, - /// Time step - pub time_zoom: Arc, - /// Auto rezoom to fit in time axis - pub time_lock: Arc, - /// Length of visible note axis - pub note_axis: Arc, - // Lowest note displayed - pub note_lo: Arc, -} - impl MidiClip { pub fn new ( name: impl AsRef, @@ -646,8 +336,6 @@ impl_has!(Sequencer: |self: Track| self.sequencer); impl_has!(Clock: |self: Sequencer| self.clock); impl_has!(Vec: |self: Sequencer| self.midi_ins); impl_has!(Vec: |self: Sequencer| self.midi_outs); -impl_has!([AtomicUsize; 2]: |self: MidiEditor| self.size); -impl_has!([AtomicUsize; 2]: |self: PianoHorizontal| self.size); impl_default!(Sequencer: Self { clock: Clock::default(), play_clip: None, @@ -863,502 +551,6 @@ impl Audio for Sequencer { } } } -impl Draw for MidiEditor { - fn draw(self, to: &mut Tui) -> Usually> { - self.tui().draw(to) - } -} -impl Draw for PianoHorizontal { - fn draw(self, to: &mut Tui) -> Usually> { - self.tui().draw(to) - } -} -impl std::fmt::Debug for MidiEditor { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("MidiEditor").field("mode", &self.mode).finish() - } -} -impl_from!(MidiEditor: |clip: &Arc>| { - let model = Self::from(Some(clip.clone())); - model.redraw(); - model -}); -impl_from!(MidiEditor: |clip: Option>>| { - let mut model = Self::default(); - *model.clip_mut() = clip; - model.redraw(); - model -}); -impl_default!(MidiEditor: Self { - size: [0, 0].into(), mode: PianoHorizontal::new(None) -}); -impl_default!(OctaveVertical: Self { - on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] -}); -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.get_time_pos(); - let note_pos = self.get_note_pos(); - let note_len = self.get_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 + 1) % clip.length); - } - redraw = true; - } - if redraw { - self.mode.redraw(); - } - } - fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } - fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } - fn note_length (&self) -> usize { self.get_note_len() } - fn note_pos (&self) -> usize { self.get_note_pos() } - fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } - fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } - fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } - fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } - fn note_len (&self) -> usize { self.get_note_len() } - fn note_len_next (&self) -> usize { self.get_note_len() + 1 } - fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } - fn note_range (&self) -> usize { self.get_note_axis() } - fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } - fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } - fn time_zoom (&self) -> usize { self.get_time_zoom() } - fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_lock (&self) -> bool { self.get_time_lock() } - fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } - fn time_pos (&self) -> usize { self.get_time_pos() } - fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } - fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } - fn time_pos_prev (&self) -> usize { - let step = self.get_note_len(); - self.get_time_pos().overflowing_sub(step) - .0.min(self.clip_length().saturating_sub(step)) - } - fn time_pos_prev_fine (&self) -> usize { - self.get_time_pos().overflowing_sub(1) - .0.min(self.clip_length().saturating_sub(1)) - } - pub fn clip_status (&self) -> impl Draw + '_ { - 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) }; - w_exact(20, south!( - w_full(origin_w(east( - button_2("f2", "name ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), - w_full(origin_w(east( - button_2("l", "ength ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), - w_full(origin_w(east( - button_2("r", "epeat ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), - )) - } - pub fn edit_status (&self) -> impl Draw + '_ { - 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.get_time_pos(); - let time_zoom = self.get_time_zoom(); - let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; - let note_pos = self.get_note_pos(); - let note_name = format!("{:4}", note_pitch_to_name(note_pos)); - let note_pos = format!("{:>3}", note_pos); - let note_len = format!("{:>4}", self.get_note_len()); - w_exact(20, south!( - w_full(origin_w(east( - button_2("t", "ime ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), - format!("{length} /{time_zoom} +{time_pos} "))))))), - w_full(origin_w(east( - button_2("z", "lock ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), - format!("{time_lock}"))))))), - w_full(origin_w(east( - button_2("x", "note ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), - 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) -> &AtomicUsize { self.mode.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } -} - -impl TimePoint for MidiEditor { - fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } -} - -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) } -} - -impl MidiEditor { - fn tui (&self) -> impl Draw { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } -} - - -impl PianoHorizontal { - pub fn new (clip: Option<&Arc>>) -> Self { - let size = [0, 0].into(); - let mut range = MidiSelection::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: MidiCursor::default(), - clip: clip.cloned(), - color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), - }; - piano.redraw(); - piano - } -} - -impl PianoHorizontal { - fn tui (&self) -> impl Draw { - south( - east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), - east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(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, note_point: usize, time_point: 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(); - if note == (127-note_point) || time == time_point { - cell.set_bg(Rgb(0,0,0)); - } else { - cell.set_bg(clip.color.darkest.term); - } - if time % 384 == 0 { - cell.set_fg(clip.color.darker.term); - cell.set_char('│'); - } else if time % 96 == 0 { - cell.set_fg(clip.color.dark.term); - cell.set_char('╎'); - } else if time % note_len == 0 { - cell.set_fg(clip.color.darker.term); - cell.set_char('┊'); - } else if (127 - note) % 12 == 0 { - cell.set_fg(clip.color.darker.term); - cell.set_char('='); - } else if (127 - note) % 6 == 0 { - cell.set_fg(clip.color.darker.term); - cell.set_char('—'); - } else { - cell.set_fg(clip.color.darker.term); - 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.term);//.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 Draw { - let time_start = self.get_time_start(); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - let buffer = self.buffer.clone(); - Thunk::new(move|to: &mut Tui|{ - let source = buffer.read().unwrap(); - let XYWH(x0, y0, w, _h) = to.area(); - //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 Draw { - let note_hi = self.get_note_hi(); - let note_lo = self.get_note_lo(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let time_pos = self.get_time_pos(); - let time_start = self.get_time_start(); - let time_zoom = self.get_time_zoom(); - let style = Some(Style::default().fg(self.color.lightest.term)); - Thunk::new(move|to: &mut Tui|{ - let XYWH(x0, y0, w, _) = to.area(); - 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 Draw { - let state = self; - let color = state.color; - let note_lo = state.get_note_lo(); - let note_hi = state.get_note_hi(); - let note_pos = state.get_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.term).bold()); - h_full(w_exact(self.keys_width, Thunk::new(move|to: &mut Tui|{ - let XYWH(x, y0, _w, _h) = to.area(); - 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(¬e_pitch_to_name(note), x, screen_y, off_style) - }; - } - }))) - } - fn timeline (&self) -> impl Draw + '_ { - w_full(h_exact(1, Thunk::new(move|to: &mut Tui|{ - let XYWH(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); - } - } - }))) - } -} - -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) -> &AtomicUsize { self.point.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } -} - -impl TimePoint for PianoHorizontal { - fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } -} - -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 time_zoom = self.get_time_zoom(); - self.time_len().set(clip.length); - PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); - 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 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() - } -} -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 OctaveVertical { - fn tui (&self) -> impl Draw { - east!( - 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), "▟"), - ) - } -} -impl_from!(MidiSelection: |data:(usize, bool)| Self { - time_len: Arc::new(0.into()), - note_axis: Arc::new(0.into()), - note_lo: Arc::new(0.into()), - time_axis: Arc::new(0.into()), - time_start: Arc::new(0.into()), - time_zoom: Arc::new(data.0.into()), - time_lock: Arc::new(data.1.into()), -}); -impl_default!(MidiCursor: Self { - time_pos: Arc::new(0.into()), - note_pos: Arc::new(36.into()), - note_len: Arc::new(24.into()), -}); - -impl NotePoint for MidiCursor { - fn note_len (&self) -> &AtomicUsize { - &self.note_len - } - fn note_pos (&self) -> &AtomicUsize { - &self.note_pos - } -} - -impl TimePoint for MidiCursor { - fn time_pos (&self) -> &AtomicUsize { - self.time_pos.as_ref() - } -} - -impl TimeRange for MidiSelection { - fn time_len (&self) -> &AtomicUsize { &self.time_len } - fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } - fn time_lock (&self) -> &AtomicBool { &self.time_lock } - fn time_start (&self) -> &AtomicUsize { &self.time_start } - fn time_axis (&self) -> &AtomicUsize { &self.time_axis } -} - -impl NoteRange for MidiSelection { - fn note_lo (&self) -> &AtomicUsize { &self.note_lo } - fn note_axis (&self) -> &AtomicUsize { &self.note_axis } -} impl Iterator for Ticker { type Item = (usize, usize); @@ -1382,7 +574,7 @@ impl Iterator for Ticker { } } -fn to_key (note: usize) -> &'static str { +pub fn to_key (note: usize) -> &'static str { match note % 12 { 11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌", 10 | 8 | 6 | 3 | 1 => " ", diff --git a/src/tek.rs b/src/tek.rs index 89e49323..2c6476cf 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -46,6 +46,7 @@ pub mod clock; pub mod config; pub mod device; pub mod dialog; +pub mod editor; pub mod menu; pub mod mix; pub mod mode; @@ -57,16 +58,17 @@ pub mod view; #[cfg(feature = "plugin")] pub mod plugin; use clap::{self, Parser, Subcommand}; -use builder_pattern::Builder; use self::{ - arrange::*, clock::*, dialog::*, browse::*, select::*, sequence::*, device::*, - config::*, mode::*, view::*, bind::*, sample::*, menu::* + config::*, mode::*, view::*, bind::*, + dialog::*, browse::*, menu::*, + clock::*, sequence::*, editor::*, + arrange::*, select::*, device::*, sample::*, }; extern crate xdg; pub(crate) use ::xdg::BaseDirectories; pub extern crate atomic_float; -pub(crate) use atomic_float::AtomicF64; +//pub(crate) use atomic_float::AtomicF64; //pub extern crate jack; //pub(crate) use ::jack::{*, contrib::{*, ClosureProcessHandler}}; pub extern crate midly; @@ -198,34 +200,39 @@ fn tek_dec (state: &mut App, axis: &ControlAxis) -> Perhaps { }) } -fn collect_commands (app: &App, input: &TuiIn) -> Usually> { - let mut commands = vec![]; - for id in app.mode.keys.iter() { - if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) - && let Some(bindings) = event_map.query(input.event()) { - for binding in bindings { - for command in binding.commands.iter() { - if let Some(command) = app.namespace(command)? as Option { - commands.push(command) - } - } - } - } - } - Ok(commands) -} - -fn execute_commands ( - app: &mut App, commands: Vec -) -> Usually)>> { - let mut history = vec![]; - for command in commands.into_iter() { - let result = command.act(app); - match result { Err(err) => { history.push((command, None)); return Err(err) } - Ok(undo) => { history.push((command, undo)); } }; - } - Ok(history) -} +//impl_handle!(TuiIn: |self: App, input|{ + //let commands = tek_collect_commands(self, input)?; + //let history = tek_execute_commands(self, commands)?; + //self.history.extend(history.into_iter()); + //Ok(None) +//}); +//fn tek_collect_commands (app: &App, input: &TuiIn) -> Usually> { + //let mut commands = vec![]; + //for id in app.mode.keys.iter() { + //if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) + //&& let Some(bindings) = event_map.query(input.event()) { + //for binding in bindings { + //for command in binding.commands.iter() { + //if let Some(command) = app.namespace(command)? as Option { + //commands.push(command) + //} + //} + //} + //} + //} + //Ok(commands) +//} +//fn tek_execute_commands ( + //app: &mut App, commands: Vec +//) -> Usually)>> { + //let mut history = vec![]; + //for command in commands.into_iter() { + //let result = command.act(app); + //match result { Err(err) => { history.push((command, None)); return Err(err) } + //Ok(undo) => { history.push((command, undo)); } }; + //} + //Ok(history) +//} pub fn tek_jack_process (app: &mut App, client: &Client, scope: &ProcessScope) -> Control { let t0 = app.perf.get_t0(); @@ -590,7 +597,7 @@ pub(crate) const HEADER: &'static str = r#" /// Must not be dropped for the duration of the process pub jack: Jack<'static>, /// Display size - pub size: [AtomicUsize;2], + pub size: Size, /// Performance counter pub perf: PerfModel, /// Available view modes and input bindings @@ -613,7 +620,7 @@ impl_has!(Vec: |self: App|self.project.midi_ins); impl_has!(Vec: |self: App|self.project.midi_outs); impl_has!(Dialog: |self: App|self.dialog); impl_has!(Jack<'static>: |self: App|self.jack); -impl_has!([AtomicUsize;2]: |self: App|self.size); +impl_has!(Size: |self: App|self.size); impl_has!(Pool: |self: App|self.pool); impl_has!(Selection: |self: App|self.project.selection); impl_as_ref!(Vec: |self: App|self.project.as_ref()); @@ -622,17 +629,11 @@ impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); impl_has_clips!( |self: App|self.pool.clips); impl_audio!(App: tek_jack_process, tek_jack_event); -impl_handle!(TuiIn: |self: App, input|{ - let commands = collect_commands(self, input)?; - let history = execute_commands(self, commands)?; - self.history.extend(history.into_iter()); - Ok(None) -}); namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| { ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), - ":h/sample-detail" => 6.max(app.measure_height() as u16 * 3 / 9), }; }); + ":h/sample-detail" => 6.max(app.size.h() as u16 * 3 / 9), }; }); namespace!(App: isize { literal = |dsl|try_to_isize(dsl); }); namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| { ":scene-count" => app.scenes().len(), @@ -690,7 +691,7 @@ namespace!(App: Option>> { }; }); -pub trait HasClipsSize { fn clips_size (&self) -> &[AtomicUsize;2]; } +pub trait HasClipsSize { fn clips_size (&self) -> &Size; } pub trait HasDevices: AsRef> + AsMut> { fn devices (&self) -> &Vec { self.as_ref() } @@ -966,7 +967,7 @@ impl Draw for App { } } } -impl HasClipsSize for App { fn clips_size (&self) -> &[AtomicUsize;2] { &self.project.size_inner } } +impl HasClipsSize for App { fn clips_size (&self) -> &Size { &self.project.size_inner } } impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } impl_default!(AppCommand: Self::Nop); primitive!(u8: try_to_u8); diff --git a/tengri b/tengri index a06ea2ac..a93fe92a 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit a06ea2ac139d09ab9eba5931455c43a3a75f4151 +Subproject commit a93fe92a596b8a9bcec898e228fca909c0e2e9f4