mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 12:16:42 +01:00
show note range in midi editor
This commit is contained in:
parent
286dec0f40
commit
eafc06edc6
5 changed files with 132 additions and 96 deletions
|
|
@ -43,6 +43,7 @@ submod! {
|
||||||
edn
|
edn
|
||||||
engine
|
engine
|
||||||
focus
|
focus
|
||||||
|
pitch
|
||||||
space
|
space
|
||||||
time
|
time
|
||||||
tui
|
tui
|
||||||
|
|
|
||||||
23
crates/tek_core/src/pitch.rs
Normal file
23
crates/tek_core/src/pitch.rs
Normal 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",
|
||||||
|
];
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
|
||||||
},
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue