show note range in midi editor

This commit is contained in:
🪞👃🪞 2024-11-29 11:13:44 +01:00
parent 286dec0f40
commit eafc06edc6
5 changed files with 132 additions and 96 deletions

View file

@ -43,6 +43,7 @@ submod! {
edn edn
engine engine
focus focus
pitch
space space
time time
tui tui

View file

@ -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",
];

View file

@ -376,6 +376,7 @@ pub fn pulses_to_name (pulses: usize) -> &'static str {
"" ""
} }
/// Performance counter
pub struct PerfModel { pub struct PerfModel {
pub enabled: bool, pub enabled: bool,
clock: quanta::Clock, clock: quanta::Clock,

View file

@ -22,7 +22,6 @@ impl InputToCommand<Tui, PhraseEditorModel> for PhraseCommand {
use KeyCode::{Char, Enter, Esc, Up, Down, PageUp, PageDown, Left, Right}; use KeyCode::{Char, Enter, Esc, Up, Down, PageUp, PageDown, Left, Right};
Some(match from.event() { Some(match from.event() {
key!(Char('`')) => ToggleDirection, key!(Char('`')) => ToggleDirection,
key!(Enter) => SetEditMode(PhraseEditMode::Note),
key!(Esc) => SetEditMode(PhraseEditMode::Scroll), key!(Esc) => SetEditMode(PhraseEditMode::Scroll),
key!(Char('-')) => SetTimeZoom( key!(Char('-')) => SetTimeZoom(
next_note_length(state.time_scale.load(Ordering::Relaxed)) next_note_length(state.time_scale.load(Ordering::Relaxed))
@ -38,6 +37,7 @@ impl InputToCommand<Tui, PhraseEditorModel> for PhraseCommand {
), ),
_ => match state.edit_mode { _ => match state.edit_mode {
PhraseEditMode::Scroll => match from.event() { PhraseEditMode::Scroll => match from.event() {
key!(Char('e')) => SetEditMode(PhraseEditMode::Note),
key!(Up) => SetNoteCursor( key!(Up) => SetNoteCursor(
state.note_point.load(Ordering::Relaxed) + 1 state.note_point.load(Ordering::Relaxed) + 1
), ),
@ -59,6 +59,7 @@ impl InputToCommand<Tui, PhraseEditorModel> for PhraseCommand {
_ => return None _ => return None
}, },
PhraseEditMode::Note => match from.event() { PhraseEditMode::Note => match from.event() {
key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll),
key!(Up) => SetNoteScroll( key!(Up) => SetNoteScroll(
state.note_start.load(Ordering::Relaxed) + 1 state.note_start.load(Ordering::Relaxed) + 1
), ),
@ -134,6 +135,7 @@ impl Command<PhraseEditorModel> for PhraseCommand {
None None
}, },
SetNoteCursor(note) => { SetNoteCursor(note) => {
let note = 127.min(note);
let start = state.note_start.load(Ordering::Relaxed); let start = state.note_start.load(Ordering::Relaxed);
state.note_point.store(note, Ordering::Relaxed); state.note_point.store(note, Ordering::Relaxed);
if note < start { if note < start {

View file

@ -1,43 +1,53 @@
use crate::*; use crate::*;
pub struct PhraseView<'a> { pub struct PhraseView<'a> {
pub(crate) focused: bool, pub(crate) focused: bool,
pub(crate) entered: bool, pub(crate) entered: bool,
pub(crate) phrase: &'a Option<Arc<RwLock<Phrase>>>, pub(crate) phrase: &'a Option<Arc<RwLock<Phrase>>>,
pub(crate) size: &'a Measure<Tui>, pub(crate) size: &'a Measure<Tui>,
pub(crate) keys: &'a Buffer, pub(crate) keys: &'a Buffer,
pub(crate) buffer: &'a BigBuffer, pub(crate) buffer: &'a BigBuffer,
pub(crate) note_len: usize, pub(crate) note_len: usize,
pub(crate) now: &'a Arc<Pulse>, pub(crate) now: &'a Arc<Pulse>,
pub(crate) note_start: &'a AtomicUsize, pub(crate) note_start: &'a AtomicUsize,
pub(crate) note_point: &'a AtomicUsize, pub(crate) note_point: usize,
pub(crate) note_clamp: &'a AtomicUsize, pub(crate) note_clamp: usize,
pub(crate) time_start: &'a AtomicUsize, note_range: (&'a str, &'a str),
pub(crate) time_point: &'a AtomicUsize,
pub(crate) time_clamp: &'a AtomicUsize, pub(crate) time_start: usize,
pub(crate) time_scale: &'a AtomicUsize, 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> { impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> {
fn from (state: &'a T) -> Self { 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 { Self {
focused: state.editor_focused(), focused: state.editor_focused(),
entered: state.editor_entered(), entered: state.editor_entered(),
note_len: state.editor().note_len, note_len: state.editor().note_len,
phrase: &state.editor().phrase, phrase: &state.editor().phrase,
size: &state.editor().size, size: &state.editor().size,
keys: &state.editor().keys, keys: &state.editor().keys,
buffer: &state.editor().buffer, buffer: &state.editor().buffer,
now: &state.editor().now, now: &state.editor().now,
note_start: &state.editor().note_start, note_start: &state.editor().note_start,
note_point: &state.editor().note_point, note_point: state.editor().note_point.load(Ordering::Relaxed),
note_clamp: &state.editor().note_clamp, note_clamp,
time_start: &state.editor().time_start, note_range,
time_point: &state.editor().time_point,
time_clamp: &state.editor().time_clamp, time_start: state.editor().time_start.load(Ordering::Relaxed),
time_scale: &state.editor().time_scale, 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> { impl<'a> Content for PhraseView<'a> {
type Engine = Tui; type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> { fn content (&self) -> impl Widget<Engine = Tui> {
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_start = self.note_start.load(Ordering::Relaxed);
let note_point = self.note_point.load(Ordering::Relaxed); let keys = move|to: &mut TuiOutput|Ok(if to.area().h() >= 2 {
let note_clamp = self.note_clamp.load(Ordering::Relaxed); to.buffer_update(to.area().set_w(5), &|cell, x, y|{
let time_start = self.time_start.load(Ordering::Relaxed); let y = y + (note_start / 2) as u16;
let time_point = self.time_point.load(Ordering::Relaxed); if x < keys.area.width && y < keys.area.height {
let time_clamp = self.time_clamp.load(Ordering::Relaxed); *cell = keys.get(x, y).clone()
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 notes_bg_null = Color::Rgb(28, 35, 25); 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 area = to.area();
let h = area.h() as usize; let h = area.h() as usize;
size.set_wh(area.w(), h); 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|{ let cursor = move|to: &mut TuiOutput|Ok(if *focused && *entered {
Ok(if *focused && *entered { let area = to.area();
let area = to.area(); let x1 = area.x() + (time_point / time_scale) as u16;
let x1 = area.x() + (time_point / time_scale) as u16; let x2 = x1 + (note_len / 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 y = area.y() + note_point.saturating_sub(note_start) as u16 / 2; let c = if note_point % 2 == 0 { "" } else { "" };
let c = if note_point % 2 == 0 { "" } else { "" }; for x in x1..x2 {
for x in x1..x2 { to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0))));
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_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_active = playhead_inactive.clone().yellow().bold().not_dim();
let playhead = CustomWidget::new( let playhead = move|to: &mut TuiOutput|{
|to:[u16;2]|Ok(Some(to.clip_h(1))), if let Some(_) = phrase {
move|to: &mut TuiOutput|{ let now = now.get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length;
if let Some(_) = phrase { let time_clamp = time_clamp;
let now = now.get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length; for x in 0..(time_clamp/time_scale).saturating_sub(*time_start) {
let time_clamp = time_clamp; let this_step = time_start + (x + 0) * time_scale;
for x in 0..(time_clamp/time_scale).saturating_sub(time_start) { let next_step = time_start + (x + 1) * time_scale;
let this_step = time_start + (x + 0) * time_scale; let x = to.area().x() + x as u16;
let next_step = time_start + (x + 1) * time_scale; let active = this_step <= now && now < next_step;
let x = to.area().x() + x as u16; let character = if active { "|" } else { "·" };
let active = this_step <= now && now < next_step; let style = if active { playhead_active } else { playhead_inactive };
let character = if active { "|" } else { "·" }; to.blit(&character, x, to.area.y(), Some(style));
let style = if active { playhead_active } else { playhead_inactive };
to.blit(&character, x, to.area.y(), Some(style));
}
} }
Ok(())
} }
).push_x(6).align_sw(); 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_color = if *focused{Color::Rgb(100, 110, 40)}else{Color::Rgb(70, 80, 50)};
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); let title_color = if *focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)};
let note_area = lay!(notes, cursor).fill_x(); let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
let piano_roll = row!(keys, note_area).fill_x(); let note_area = lay!(
let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border); CustomWidget::new(|to|Ok(Some(to)), notes).fill_x(),
let content = lay!(content, playhead); 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(); let mut name = "".to_string();
if let Some(phrase) = phrase { if let Some(phrase) = phrase {
name = phrase.read().unwrap().name.clone(); 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()); 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: {} (+{}:{}*{}|{})", //lower_right = format!("Zoom: {} (+{}:{}*{}|{})",
//pulses_to_name(time_scale), //pulses_to_name(time_scale),
//time_start, time_point.unwrap_or(0), //time_start, time_point.unwrap_or(0),
@ -159,7 +167,8 @@ impl<'a> Content for PhraseView<'a> {
}; };
lay!( lay!(
content, 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(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(), 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| { buffer_update(&mut buffer, area, &|cell, x, y| {
let y = 63 - y; let y = 63 - y;
match x { 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('▀'); cell.set_char('▀');
let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize]; let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize];
cell.set_fg(fg); cell.set_fg(fg);
cell.set_bg(bg); cell.set_bg(bg);
}, },
1 => { 3 => {
cell.set_char('▀'); cell.set_char('▀');
cell.set_fg(Color::White); cell.set_fg(Color::White);
cell.set_bg(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]);
},
_ => {} _ => {}
} }
}); });