From c737c7d8393cd1a0c1587787f84c0c8d0dc383dd Mon Sep 17 00:00:00 2001 From: i do not exist Date: Wed, 24 Jun 2026 10:18:45 +0300 Subject: [PATCH] restruct: 30e --- src/app.rs | 11 +- src/app/cli.rs | 9 +- src/app/config.rs | 2 +- src/device.rs | 1 + src/device/clock.rs | 28 +- src/device/editor.rs | 755 ++++++------------------------------ src/device/editor/octave.rs | 36 ++ src/device/editor/piano.rs | 302 +++++++++++++++ src/device/editor/point.rs | 76 ++++ src/device/editor/range.rs | 124 ++++++ src/device/meter.rs | 84 ++++ src/device/mix.rs | 82 ---- src/tek.rs | 8 +- 13 files changed, 771 insertions(+), 747 deletions(-) create mode 100644 src/device/editor/octave.rs create mode 100644 src/device/editor/piano.rs create mode 100644 src/device/editor/point.rs create mode 100644 src/device/editor/range.rs create mode 100644 src/device/meter.rs diff --git a/src/app.rs b/src/app.rs index 85922694..b7820826 100644 --- a/src/app.rs +++ b/src/app.rs @@ -87,11 +87,11 @@ impl App { pub fn new_shared_run ( exit: &Exit, jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef - ) -> Usually<(Task, Task)> { + ) -> Usually<(Self, Task, Task)> { let state = Self::new_shared(&jack, project, config, mode); let keyboard = tui_input(&exit, &state, Duration::from_millis(100))?; let terminal = tui_output(&exit, &state, Duration::from_millis(10))?; - Ok((keyboard, terminal)) + Ok((state, keyboard, terminal)) } pub fn confirm (&mut self) -> Perhaps { @@ -361,10 +361,10 @@ impl HasJack<'static> for App { def_command!(AppCommand: |app: App| { Nop => Ok(None), - Confirm => tek_confirm(app), Cancel => todo!(), // TODO delegate: - Inc { axis: ControlAxis } => tek_inc(app, axis), - Dec { axis: ControlAxis } => tek_dec(app, axis), + Confirm => app.confirm(), + Inc { axis: ControlAxis } => app.inc(axis), + Dec { axis: ControlAxis } => app.dec(axis), SetDialog { dialog: Dialog } => { swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) }, @@ -536,6 +536,7 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua h_exact(2, origin_n(w_full(item))) ))); } + Ok(to.area().into()) }))) } else { None diff --git a/src/app/cli.rs b/src/app/cli.rs index 1698d68f..78dea7a7 100644 --- a/src/app/cli.rs +++ b/src/app/cli.rs @@ -135,7 +135,7 @@ impl Action { } pub fn tek_project_new ( - jack: &Jack, + jack: &Jack<'static>, clock: Clock, left_from: &[impl AsRef], left_to: &[impl AsRef], @@ -154,9 +154,7 @@ pub fn tek_project_new ( let right_tos = Connect::collect(right_to, empty, empty); let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; - - // Create initial project: - let mut project = Arrangement::new( + Ok(Arrangement::new( jack, None, clock, @@ -168,8 +166,7 @@ pub fn tek_project_new ( Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) .collect::, _>>()? - ); - Ok(project) + )) } pub fn tek_show_version () { diff --git a/src/app/config.rs b/src/app/config.rs index 172a2b55..3ec4f94a 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -37,7 +37,7 @@ impl Config { const CONFIG: &'static str = "tek.edn"; const DEFAULTS: &'static str = include_str!("../tek.edn"); - pub fn watch (callback: impl Fn(Self)->T) -> Usually { + pub fn watch (callback: impl FnOnce(Self)->T) -> Usually { let config = Self::init_new(None)?; let result = callback(config); Ok(result) diff --git a/src/device.rs b/src/device.rs index 83cc1ad3..d271ec7c 100644 --- a/src/device.rs +++ b/src/device.rs @@ -106,6 +106,7 @@ pub mod clock; pub use self::clock::*; pub mod dialog; pub use self::dialog::*; pub mod editor; pub use self::editor::*; pub mod menu; pub use self::menu::*; +pub mod meter; pub use self::meter::*; pub mod mix; pub use self::mix::*; pub mod port; pub use self::port::*; pub mod sample; pub use self::sample::*; diff --git a/src/device/clock.rs b/src/device/clock.rs index 9c78dc96..6aedab97 100644 --- a/src/device/clock.rs +++ b/src/device/clock.rs @@ -387,16 +387,26 @@ impl ClockView { let lat = chunk / rate * 1000.; let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); let mut cache = cache.write().unwrap(); - cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); - cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); - cache.sr.update(Some((compact, rate)), |buf,_,_|{ - buf.clear(); - if compact { - write!(buf, "{:.1}kHz", rate / 1000.) - } else { - write!(buf, "{:.0}Hz", rate) + + cache.buf.update( + Some(chunk), rewrite!(buf, "{chunk}") + ); + + cache.lat.update( + Some(lat), rewrite!(buf, "{lat:.1}ms") + ); + + cache.sr.update( + Some((compact, rate)), |buf: &mut String,_,_|{ + buf.clear(); + if compact { + write!(buf, "{:.1}kHz", rate / 1000.) + } else { + write!(buf, "{:.0}Hz", rate) + } } - }); + ); + if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { let pulse = clock.timebase.usecs_to_pulse(now); let time = now/1000000.; diff --git a/src/device/editor.rs b/src/device/editor.rs index adf787d2..2ab03f36 100644 --- a/src/device/editor.rs +++ b/src/device/editor.rs @@ -22,82 +22,116 @@ pub struct MidiEditor { 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, -} +impl_default!(MidiEditor: Self { + size: [0, 0].into(), mode: PianoHorizontal::new(None) +}); -/// 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, +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(fg(Rgb(255, 255, 255), format!("{name} "))))))), + w_full(origin_w(east( + button_2("l", "ength ", false), + w_full(origin_e(fg(Rgb(255, 255, 255), format!("{length} "))))))), + w_full(origin_w(east( + button_2("r", "epeat ", false), + w_full(origin_e(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(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(fg(Rgb(255, 255, 255), + format!("{time_lock}"))))))), + w_full(origin_w(east( + button_2("x", "note ", false), + w_full(origin_e(fg(Rgb(255, 255, 255), + format!("{note_name} {note_pos} {note_len}"))))))), + )) + } } /// ``` @@ -217,257 +251,32 @@ pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { } } -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().load(Relaxed) + self.time_axis().load(Relaxed) * self.time_zoom().load(Relaxed) - } -} - -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.mode.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(fg(Rgb(255, 255, 255), format!("{name} "))))))), - w_full(origin_w(east( - button_2("l", "ength ", false), - w_full(origin_e(fg(Rgb(255, 255, 255), format!("{length} "))))))), - w_full(origin_w(east( - button_2("r", "epeat ", false), - w_full(origin_e(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(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(fg(Rgb(255, 255, 255), - format!("{time_lock}"))))))), - w_full(origin_w(east( - button_2("x", "note ", false), - w_full(origin_e(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() } @@ -494,339 +303,7 @@ impl View for MidiEditor { } } - -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(move|to: &mut Tui|{ - let xywh = to.area().into(); - let XYWH(x0, y0, w, _h) = xywh; - let source = buffer.read().unwrap(); - //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.0.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { - *cell = source_cell.clone(); - } - } - } - } - } - Ok(xywh) - }) - } - - 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(move|to: &mut Tui|{ - let xywh = to.area().into(); - let XYWH(x0, y0, w, _h) = xywh; - 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 - } - } - Ok(xywh) - }) - } - - 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(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(move|to: &mut Tui|{ - let xywh = to.area().into(); - let XYWH(x, y0, _w, _h) = xywh; - 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) - }; - } - Ok(xywh) - }))) - } - - fn timeline (&self) -> impl Draw + '_ { - w_full(h_exact(1, thunk(move|to: &mut Tui|{ - let xywh = to.area().into(); - let XYWH(x, y, w, _h) = xywh; - 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().load(Relaxed); - if t < length { - to.blit(&"|", screen_x, y, style); - } - } - Ok(xywh) - }))) - } -} - -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().load(Relaxed), 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().store(clip.length, Relaxed); - 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!( - fg_bg(self.color(0), self.color(1), "▙"), - fg_bg(self.color(2), self.color(3), "▙"), - fg_bg(self.color(4), self.color(5), "▌"), - fg_bg(self.color(6), self.color(7), "▟"), - fg_bg(self.color(8), self.color(9), "▟"), - 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 } -} +mod octave; pub use self::octave::*; +mod piano; pub use self::piano::*; +mod point; pub use self::point::*; +mod range; pub use self::range::*; diff --git a/src/device/editor/octave.rs b/src/device/editor/octave.rs new file mode 100644 index 00000000..21124e98 --- /dev/null +++ b/src/device/editor/octave.rs @@ -0,0 +1,36 @@ +use crate::*; + +/// 12 piano keys, some highlighted. +/// +/// ``` +/// let keys = tek::OctaveVertical::default(); +/// ``` +#[derive(Copy, Clone)] +pub struct OctaveVertical { + pub on: [bool; 12], + pub colors: [Color; 3] +} + +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 { + pub fn tui (&self) -> impl Draw { + east!( + fg_bg(self.color(0), self.color(1), "▙"), + fg_bg(self.color(2), self.color(3), "▙"), + fg_bg(self.color(4), self.color(5), "▌"), + fg_bg(self.color(6), self.color(7), "▟"), + fg_bg(self.color(8), self.color(9), "▟"), + fg_bg(self.color(10), self.color(11), "▟"), + ) + } +} + +impl_default!(OctaveVertical: Self { + on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] +}); diff --git a/src/device/editor/piano.rs b/src/device/editor/piano.rs new file mode 100644 index 00000000..d1c6fbd4 --- /dev/null +++ b/src/device/editor/piano.rs @@ -0,0 +1,302 @@ +use crate::*; + +/// 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, +} + +impl_has!(Size: |self: PianoHorizontal| self.size); +impl Draw for PianoHorizontal { + fn draw (self, to: &mut Tui) -> Usually> { + self.tui().draw(to) + } +} + +impl PianoHorizontal { + + pub fn new (clip: Option<&Arc>>) -> Self { + let size: 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 + } + + pub 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())))), + ) + } + + /// 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(move|to: &mut Tui|{ + let xywh = to.area().into(); + let XYWH(x0, y0, w, _h) = xywh; + let source = buffer.read().unwrap(); + //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.0.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { + *cell = source_cell.clone(); + } + } + } + } + } + Ok(xywh) + }) + } + + 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(move|to: &mut Tui|{ + let xywh = to.area().into(); + let XYWH(x0, y0, w, _h) = xywh; + 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 + } + } + Ok(xywh) + }) + } + + 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(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(move|to: &mut Tui|{ + let xywh = to.area().into(); + let XYWH(x, y0, _w, _h) = xywh; + 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) + }; + } + Ok(xywh) + }))) + } + + fn timeline (&self) -> impl Draw + '_ { + w_full(h_exact(1, thunk(move|to: &mut Tui|{ + let xywh = to.area().into(); + let XYWH(x, y, w, _h) = xywh; + 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().load(Relaxed); + if t < length { + to.blit(&"|", screen_x, y, style); + } + } + Ok(xywh) + }))) + } +} + +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().load(Relaxed), 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().store(clip.length, Relaxed); + 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() + } +} diff --git a/src/device/editor/point.rs b/src/device/editor/point.rs new file mode 100644 index 00000000..ee1b0b0e --- /dev/null +++ b/src/device/editor/point.rs @@ -0,0 +1,76 @@ +use crate::*; + +/// +/// ``` +/// 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, +} + +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() + } +} + +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() + } +} diff --git a/src/device/editor/range.rs b/src/device/editor/range.rs new file mode 100644 index 00000000..973a1e14 --- /dev/null +++ b/src/device/editor/range.rs @@ -0,0 +1,124 @@ +use crate::*; + +/// +/// ``` +/// 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, +} + +pub trait MidiRange: TimeRange + NoteRange {} + +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_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 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 } +} + +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().load(Relaxed) + self.time_axis().load(Relaxed) * self.time_zoom().load(Relaxed) + } +} + +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) + } +} diff --git a/src/device/meter.rs b/src/device/meter.rs new file mode 100644 index 00000000..bfe9a48a --- /dev/null +++ b/src/device/meter.rs @@ -0,0 +1,84 @@ +use crate::*; + +#[derive(Debug, Default)] pub enum MeteringMode { + #[default] Rms, + Log10, +} + +#[derive(Debug, Default, Clone)] +pub struct Log10Meter(pub f32); + +#[derive(Debug, Default, Clone)] +pub struct RmsMeter(pub f32); + +impl Draw for RmsMeter { + fn draw (self, to: &mut Tui) -> Usually> { + let XYWH(x, y, w, h) = to.area().into(); + let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); + } + } + Ok(to.area().into()) + } +} + +impl Draw for Log10Meter { + fn draw(self, to: &mut Tui) -> Usually> { + let XYWH(x, y, w, h) = to.area().into(); + let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32 / 100.0).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2 - y, Some(Style::default().green())); + } + } + Ok(to.area().into()) + } +} + +fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { + bg(Black, w_exact(2, iter_east_fixed(1, ||meters.iter(), |value, _index|{ + h_full(RmsMeter(*value)) + }))) +} + +pub fn to_log10 (samples: &[f32]) -> f32 { + let total: f32 = samples.iter().map(|x|x.abs()).sum(); + let count = samples.len() as f32; + 10. * (total / count).log10() +} + +pub fn to_rms (samples: &[f32]) -> f32 { + let sum = samples.iter() + .map(|s|*s) + .reduce(|sum, sample|sum + sample.abs()) + .unwrap_or(0.0); + (sum / samples.len() as f32).sqrt() +} + +#[cfg(test)] mod test_view_meter { + use super::*; + use proptest::prelude::*; + proptest! { + + #[test] fn proptest_view_meter ( + label in "\\PC*", value in f32::MIN..f32::MAX + ) { + let _ = view_meter(&label, value); + } + + #[test] fn proptest_view_meters ( + value1 in f32::MIN..f32::MAX, + value2 in f32::MIN..f32::MAX + ) { + let _ = view_meters(&[value1, value2]); + } + } +} diff --git a/src/device/mix.rs b/src/device/mix.rs index 860a7845..7e4d5574 100644 --- a/src/device/mix.rs +++ b/src/device/mix.rs @@ -1,78 +1,11 @@ use crate::*; -#[derive(Debug, Default)] pub enum MeteringMode { - #[default] Rms, - Log10, -} - -#[derive(Debug, Default, Clone)] -pub struct Log10Meter(pub f32); - -#[derive(Debug, Default, Clone)] -pub struct RmsMeter(pub f32); - #[derive(Debug, Default)] pub enum MixingMode { #[default] Summing, Average, } -#[cfg(test)] mod test_view_meter { - use super::*; - use proptest::prelude::*; - proptest! { - - #[test] fn proptest_view_meter ( - label in "\\PC*", value in f32::MIN..f32::MAX - ) { - let _ = view_meter(&label, value); - } - - #[test] fn proptest_view_meters ( - value1 in f32::MIN..f32::MAX, - value2 in f32::MIN..f32::MAX - ) { - let _ = view_meters(&[value1, value2]); - } - } -} - - impl Draw for RmsMeter { - fn draw(self, to: &mut Tui) -> Usually> { - let XYWH(x, y, w, h) = to.area(); - let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); - } - } - } - } - - impl Draw for Log10Meter { - fn draw(self, to: &mut Tui) -> Usually> { - let XYWH(x, y, w, h) = to.area(); - let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32 / 100.0).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2 - y, Some(Style::default().green())); - } - } - } - } - - fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { - bg(Black, w_exact(2, iter_east_fixed(1, ||meters.iter(), |value, _index|{ - h_full(RmsMeter(*value)) - }))) - } - pub fn mix_summing ( buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, ) -> bool { @@ -107,18 +40,3 @@ pub fn mix_average ( } true } - -pub fn to_log10 (samples: &[f32]) -> f32 { - let total: f32 = samples.iter().map(|x|x.abs()).sum(); - let count = samples.len() as f32; - 10. * (total / count).log10() -} - - -pub fn to_rms (samples: &[f32]) -> f32 { - let sum = samples.iter() - .map(|s|*s) - .reduce(|sum, sample|sum + sample.abs()) - .unwrap_or(0.0); - (sum / samples.len() as f32).sqrt() -} diff --git a/src/tek.rs b/src/tek.rs index c2697b41..3b168c6e 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -87,11 +87,10 @@ pub(crate) use tengri::{ /// Command-line entrypoint. #[cfg(feature = "cli")] pub fn main () -> Usually<()> { - Config::watch(|config|{ Exit::run(|exit|{ - Jack::new_run("tek", |jack|{ - App::new_shared_run(&exit, &jack, Arrangement::new( + Jack::new_run("tek", move|jack|{ + Ok(App::new_shared_run(&exit, &jack, Arrangement::new( &jack, None, Clock::new(&jack, Some(51.))?, @@ -99,13 +98,12 @@ pub fn main () -> Usually<()> { vec![], vec![], vec![], - ), config, ":menu") + ), config, ":menu")?.0) // TODO: Sync I/O timings with main clock, so that things // "accidentally" fall on the beat in overload conditions. }) }) })?; - Ok(()) }