From eafc06edc613931d75659d222bb6d0cd40c333e6 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Fri, 29 Nov 2024 11:13:44 +0100 Subject: [PATCH] show note range in midi editor --- crates/tek_core/src/lib.rs | 1 + crates/tek_core/src/pitch.rs | 23 ++ crates/tek_core/src/time.rs | 1 + .../tek_tui/src/tui_control_phrase_editor.rs | 4 +- crates/tek_tui/src/tui_view_phrase_editor.rs | 199 +++++++++--------- 5 files changed, 132 insertions(+), 96 deletions(-) create mode 100644 crates/tek_core/src/pitch.rs diff --git a/crates/tek_core/src/lib.rs b/crates/tek_core/src/lib.rs index f72d0ad2..e74493ce 100644 --- a/crates/tek_core/src/lib.rs +++ b/crates/tek_core/src/lib.rs @@ -43,6 +43,7 @@ submod! { edn engine focus + pitch space time tui diff --git a/crates/tek_core/src/pitch.rs b/crates/tek_core/src/pitch.rs new file mode 100644 index 00000000..6a2ac714 --- /dev/null +++ b/crates/tek_core/src/pitch.rs @@ -0,0 +1,23 @@ +use crate::*; +use midly::num::u7; + +pub fn to_note_name (n: usize) -> &'static str { + if n > 127 { + panic!("to_note_name({n}): must be 0-127"); + } + MIDI_NOTE_NAMES[n] +} + +pub const MIDI_NOTE_NAMES: [&'static str;128] = [ + "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", + "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", + "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", + "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", + "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", + "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", + "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6", + "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", + "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", + "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", + "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", +]; diff --git a/crates/tek_core/src/time.rs b/crates/tek_core/src/time.rs index da6c9518..bede1fd5 100644 --- a/crates/tek_core/src/time.rs +++ b/crates/tek_core/src/time.rs @@ -376,6 +376,7 @@ pub fn pulses_to_name (pulses: usize) -> &'static str { "" } +/// Performance counter pub struct PerfModel { pub enabled: bool, clock: quanta::Clock, diff --git a/crates/tek_tui/src/tui_control_phrase_editor.rs b/crates/tek_tui/src/tui_control_phrase_editor.rs index b54012c2..c9d82f78 100644 --- a/crates/tek_tui/src/tui_control_phrase_editor.rs +++ b/crates/tek_tui/src/tui_control_phrase_editor.rs @@ -22,7 +22,6 @@ impl InputToCommand for PhraseCommand { use KeyCode::{Char, Enter, Esc, Up, Down, PageUp, PageDown, Left, Right}; Some(match from.event() { key!(Char('`')) => ToggleDirection, - key!(Enter) => SetEditMode(PhraseEditMode::Note), key!(Esc) => SetEditMode(PhraseEditMode::Scroll), key!(Char('-')) => SetTimeZoom( next_note_length(state.time_scale.load(Ordering::Relaxed)) @@ -38,6 +37,7 @@ impl InputToCommand for PhraseCommand { ), _ => match state.edit_mode { PhraseEditMode::Scroll => match from.event() { + key!(Char('e')) => SetEditMode(PhraseEditMode::Note), key!(Up) => SetNoteCursor( state.note_point.load(Ordering::Relaxed) + 1 ), @@ -59,6 +59,7 @@ impl InputToCommand for PhraseCommand { _ => return None }, PhraseEditMode::Note => match from.event() { + key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll), key!(Up) => SetNoteScroll( state.note_start.load(Ordering::Relaxed) + 1 ), @@ -134,6 +135,7 @@ impl Command for PhraseCommand { None }, SetNoteCursor(note) => { + let note = 127.min(note); let start = state.note_start.load(Ordering::Relaxed); state.note_point.store(note, Ordering::Relaxed); if note < start { diff --git a/crates/tek_tui/src/tui_view_phrase_editor.rs b/crates/tek_tui/src/tui_view_phrase_editor.rs index 28999476..c1f82944 100644 --- a/crates/tek_tui/src/tui_view_phrase_editor.rs +++ b/crates/tek_tui/src/tui_view_phrase_editor.rs @@ -1,43 +1,53 @@ use crate::*; pub struct PhraseView<'a> { - pub(crate) focused: bool, - pub(crate) entered: bool, - pub(crate) phrase: &'a Option>>, - pub(crate) size: &'a Measure, - pub(crate) keys: &'a Buffer, - pub(crate) buffer: &'a BigBuffer, - pub(crate) note_len: usize, - pub(crate) now: &'a Arc, + pub(crate) focused: bool, + pub(crate) entered: bool, + pub(crate) phrase: &'a Option>>, + pub(crate) size: &'a Measure, + pub(crate) keys: &'a Buffer, + pub(crate) buffer: &'a BigBuffer, + pub(crate) note_len: usize, + pub(crate) now: &'a Arc, pub(crate) note_start: &'a AtomicUsize, - pub(crate) note_point: &'a AtomicUsize, - pub(crate) note_clamp: &'a AtomicUsize, + pub(crate) note_point: usize, + pub(crate) note_clamp: usize, - pub(crate) time_start: &'a AtomicUsize, - pub(crate) time_point: &'a AtomicUsize, - pub(crate) time_clamp: &'a AtomicUsize, - pub(crate) time_scale: &'a AtomicUsize, + note_range: (&'a str, &'a str), + + pub(crate) time_start: usize, + pub(crate) time_point: usize, + pub(crate) time_clamp: usize, + pub(crate) time_scale: usize, } impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { fn from (state: &'a T) -> Self { + let [w, h] = state.editor().size.wh(); + let note_start = state.editor().note_start.load(Ordering::Relaxed); + let note_clamp = state.editor().note_clamp.load(Ordering::Relaxed); + let note_end = (note_start + h.saturating_sub(2) / 2).min(127); + let note_range = (to_note_name(note_start), to_note_name(note_end)); Self { - focused: state.editor_focused(), - entered: state.editor_entered(), - note_len: state.editor().note_len, - phrase: &state.editor().phrase, - size: &state.editor().size, - keys: &state.editor().keys, - buffer: &state.editor().buffer, - now: &state.editor().now, + focused: state.editor_focused(), + entered: state.editor_entered(), + note_len: state.editor().note_len, + phrase: &state.editor().phrase, + size: &state.editor().size, + keys: &state.editor().keys, + buffer: &state.editor().buffer, + now: &state.editor().now, + note_start: &state.editor().note_start, - note_point: &state.editor().note_point, - note_clamp: &state.editor().note_clamp, - time_start: &state.editor().time_start, - time_point: &state.editor().time_point, - time_clamp: &state.editor().time_clamp, - time_scale: &state.editor().time_scale, + note_point: state.editor().note_point.load(Ordering::Relaxed), + note_clamp, + note_range, + + time_start: state.editor().time_start.load(Ordering::Relaxed), + time_point: state.editor().time_point.load(Ordering::Relaxed), + time_clamp: state.editor().time_clamp.load(Ordering::Relaxed), + time_scale: state.editor().time_scale.load(Ordering::Relaxed), } } } @@ -45,28 +55,22 @@ impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { impl<'a> Content for PhraseView<'a> { type Engine = Tui; fn content (&self) -> impl Widget { - let Self { focused, entered, phrase, size, keys, buffer, note_len, now, .. } = self; + let Self { + focused, entered, phrase, size, keys, buffer, note_len, now, + note_point, note_range, + time_start, time_point, time_clamp, time_scale, .. + } = self; let note_start = self.note_start.load(Ordering::Relaxed); - let note_point = self.note_point.load(Ordering::Relaxed); - let note_clamp = self.note_clamp.load(Ordering::Relaxed); - let time_start = self.time_start.load(Ordering::Relaxed); - let time_point = self.time_point.load(Ordering::Relaxed); - let time_clamp = self.time_clamp.load(Ordering::Relaxed); - let time_scale = self.time_scale.load(Ordering::Relaxed); - //let color = Color::Rgb(0,255,0); - //let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color); - let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{ - Ok(if to.area().h() >= 2 { - to.buffer_update(to.area().set_w(5), &|cell, x, y|{ - let y = y + (note_start / 2) as u16; - if x < keys.area.width && y < keys.area.height { - *cell = keys.get(x, y).clone() - } - }); - }) - }).fill_y(); + let keys = move|to: &mut TuiOutput|Ok(if to.area().h() >= 2 { + to.buffer_update(to.area().set_w(5), &|cell, x, y|{ + let y = y + (note_start / 2) as u16; + if x < keys.area.width && y < keys.area.height { + *cell = keys.get(x, y).clone() + } + }); + }); let notes_bg_null = Color::Rgb(28, 35, 25); - let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ + let notes = move|to: &mut TuiOutput|{ let area = to.area(); let h = area.h() as usize; size.set_wh(area.w(), h); @@ -88,54 +92,58 @@ impl<'a> Content for PhraseView<'a> { } }); }) - }).fill_x(); - let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ - Ok(if *focused && *entered { - let area = to.area(); - let x1 = area.x() + (time_point / time_scale) as u16; - let x2 = x1 + (note_len / time_scale) as u16; - let y = area.y() + note_point.saturating_sub(note_start) as u16 / 2; - let c = if note_point % 2 == 0 { "▀" } else { "▄" }; - for x in x1..x2 { - to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0)))); - } - }) + }; + let cursor = move|to: &mut TuiOutput|Ok(if *focused && *entered { + let area = to.area(); + let x1 = area.x() + (time_point / time_scale) as u16; + let x2 = x1 + (note_len / time_scale) as u16; + let y = area.y() + note_point.saturating_sub(note_start) as u16 / 2; + let c = if note_point % 2 == 0 { "▀" } else { "▄" }; + for x in x1..x2 { + to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0)))); + } }); let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30)); let playhead_active = playhead_inactive.clone().yellow().bold().not_dim(); - let playhead = CustomWidget::new( - |to:[u16;2]|Ok(Some(to.clip_h(1))), - move|to: &mut TuiOutput|{ - if let Some(_) = phrase { - let now = now.get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length; - let time_clamp = time_clamp; - for x in 0..(time_clamp/time_scale).saturating_sub(time_start) { - let this_step = time_start + (x + 0) * time_scale; - let next_step = time_start + (x + 1) * time_scale; - let x = to.area().x() + x as u16; - let active = this_step <= now && now < next_step; - let character = if active { "|" } else { "·" }; - let style = if active { playhead_active } else { playhead_inactive }; - to.blit(&character, x, to.area.y(), Some(style)); - } + let playhead = move|to: &mut TuiOutput|{ + if let Some(_) = phrase { + let now = now.get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length; + let time_clamp = time_clamp; + for x in 0..(time_clamp/time_scale).saturating_sub(*time_start) { + let this_step = time_start + (x + 0) * time_scale; + let next_step = time_start + (x + 1) * time_scale; + let x = to.area().x() + x as u16; + let active = this_step <= now && now < next_step; + let character = if active { "|" } else { "·" }; + let style = if active { playhead_active } else { playhead_inactive }; + to.blit(&character, x, to.area.y(), Some(style)); } - Ok(()) } - ).push_x(6).align_sw(); - let border_color = if *focused{Color::Rgb(100, 110, 40)}else{Color::Rgb(70, 80, 50)}; - let title_color = if *focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)}; - let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); - let note_area = lay!(notes, cursor).fill_x(); - let piano_roll = row!(keys, note_area).fill_x(); - let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border); - let content = lay!(content, playhead); + Ok(()) + }; + let border_color = if *focused{Color::Rgb(100, 110, 40)}else{Color::Rgb(70, 80, 50)}; + let title_color = if *focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)}; + let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); + let note_area = lay!( + CustomWidget::new(|to|Ok(Some(to)), notes).fill_x(), + CustomWidget::new(|to|Ok(Some(to)), cursor) + ).fill_x(); + let piano_roll = row!( + CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), keys).fill_y(), + note_area + ).fill_x().bg(Color::Rgb(40, 50, 30)).border(border); + let content = lay!( + piano_roll, + CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_h(1))), playhead).push_x(6).align_sw() + ); let mut name = "".to_string(); if let Some(phrase) = phrase { name = phrase.read().unwrap().name.clone(); } - let mut upper_left = format!("[{}] {name}", if *entered {"■"} else {" "},); + let mut upper_left = format!("{} [{}] {name}", note_range.1, if *entered {"■"} else {" "},); + let mut lower_left = format!("{}", note_range.0); let mut lower_right = format!("┤{}├", size.format()); - lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale)); + lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(*time_scale)); //lower_right = format!("Zoom: {} (+{}:{}*{}|{})", //pulses_to_name(time_scale), //time_start, time_point.unwrap_or(0), @@ -159,7 +167,8 @@ impl<'a> Content for PhraseView<'a> { }; lay!( content, - TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(), + TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw(), + TuiStyle::fg(lower_left.to_string(), title_color).push_x(1).align_sw(), TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(), TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(), ) @@ -184,23 +193,23 @@ pub(crate) fn keys_vert () -> Buffer { buffer_update(&mut buffer, area, &|cell, x, y| { let y = 63 - y; match x { - 0 => { + 0 => if y % 6 == 0 { + cell.set_char('C'); + }, + 1 => if y % 6 == 0 { + cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]); + }, + 2 => { cell.set_char('▀'); let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize]; cell.set_fg(fg); cell.set_bg(bg); }, - 1 => { + 3 => { cell.set_char('▀'); cell.set_fg(Color::White); cell.set_bg(Color::White); }, - 2 => if y % 6 == 0 { - cell.set_char('C'); - }, - 3 => if y % 6 == 0 { - cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]); - }, _ => {} } });