tek/crates/tek/src/tui/phrase_editor.rs
2024-12-10 00:10:43 +01:00

712 lines
27 KiB
Rust

use crate::*;
/// Contains state for viewing and editing a phrase
pub struct PhraseEditorModel {
/// Phrase being played
pub(crate) phrase: Option<Arc<RwLock<Phrase>>>,
/// Length of note that will be inserted, in pulses
pub(crate) note_len: usize,
/// The full piano roll is rendered to this buffer
pub(crate) buffer: BigBuffer,
/// Notes currently held at input
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
/// Current position of global playhead
pub(crate) now: Arc<Pulse>,
/// Width and height of notes area at last render
pub(crate) size: Measure<Tui>,
pub(crate) note_lo: AtomicUsize,
pub(crate) note_point: AtomicUsize,
pub(crate) time_start: AtomicUsize,
pub(crate) time_point: AtomicUsize,
pub(crate) time_scale: AtomicUsize,
pub(crate) edit_mode: PhraseEditMode,
pub(crate) view_mode: PhraseViewMode,
}
#[derive(Copy, Clone, Debug)]
pub enum PhraseEditMode {
Note,
Scroll,
}
#[derive(Copy, Clone, Debug)]
pub enum PhraseViewMode {
PianoHorizontal {
time_zoom: usize,
note_zoom: PhraseViewNoteZoom,
},
PianoVertical {
time_zoom: usize,
note_zoom: PhraseViewNoteZoom,
},
}
impl std::fmt::Debug for PhraseEditorModel {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("PhraseEditorModel")
.field("note_axis", &format!("{} {}",
self.note_lo.load(Ordering::Relaxed),
self.note_point.load(Ordering::Relaxed),
))
.field("time_axis", &format!("{} {} {}",
self.time_start.load(Ordering::Relaxed),
self.time_point.load(Ordering::Relaxed),
self.time_scale.load(Ordering::Relaxed),
))
.finish()
}
}
impl Default for PhraseEditorModel {
fn default () -> Self {
Self {
phrase: None,
note_len: 24,
buffer: Default::default(),
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
now: Pulse::default().into(),
size: Measure::new(),
edit_mode: PhraseEditMode::Scroll,
note_lo: 0.into(),
note_point: 0.into(),
time_start: 0.into(),
time_point: 0.into(),
time_scale: 24.into(),
view_mode: PhraseViewMode::PianoHorizontal {
time_zoom: 24,
note_zoom: PhraseViewNoteZoom::N(1)
},
}
}
}
impl PhraseEditorModel {
/// Put note at current position
pub fn put_note (&mut self) {
if let Some(phrase) = &self.phrase {
let time = self.time_point.load(Ordering::Relaxed);
let note = self.note_point.load(Ordering::Relaxed);
let mut phrase = phrase.write().unwrap();
let key: u7 = u7::from(note as u8);
let vel: u7 = 100.into();
let start = time;
let end = (start + self.note_len) % phrase.length;
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
self.buffer = self.view_mode.draw(&phrase);
}
}
/// Move time cursor forward by current note length
pub fn time_cursor_advance (&self) {
let point = self.time_point.load(Ordering::Relaxed);
let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
let forward = |time|(time + self.note_len) % length;
self.time_point.store(forward(point), Ordering::Relaxed);
}
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
pub fn show_phrase (&mut self, phrase: Option<Arc<RwLock<Phrase>>>) {
if phrase.is_some() {
self.buffer = self.view_mode.draw(&*phrase.as_ref().unwrap().read().unwrap());
self.phrase = phrase;
} else {
self.buffer = Default::default();
self.phrase = None;
}
}
}
pub trait HasEditor {
fn editor (&self) -> &PhraseEditorModel;
fn editor_focused (&self) -> bool;
fn editor_entered (&self) -> bool;
}
impl HasEditor for SequencerTui {
fn editor (&self) -> &PhraseEditorModel {
&self.editor
}
fn editor_focused (&self) -> bool {
self.focused() == SequencerFocus::PhraseEditor
}
fn editor_entered (&self) -> bool {
self.entered() && self.editor_focused()
}
}
impl HasEditor for ArrangerTui {
fn editor (&self) -> &PhraseEditorModel {
&self.editor
}
fn editor_focused (&self) -> bool {
self.focused() == ArrangerFocus::PhraseEditor
}
fn editor_entered (&self) -> bool {
self.entered() && self.editor_focused()
}
}
pub struct PhraseView<'a> {
focused: bool,
entered: bool,
phrase: &'a Option<Arc<RwLock<Phrase>>>,
buffer: &'a BigBuffer,
note_len: usize,
now: &'a Arc<Pulse>,
size: &'a Measure<Tui>,
view_mode: &'a PhraseViewMode,
note_point: usize,
note_range: (usize, usize),
note_names: (&'a str, &'a str),
time_start: usize,
time_point: usize,
}
impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> {
fn from (state: &'a T) -> Self {
let editor = state.editor();
let height = editor.size.h();
let note_point = editor.note_point.load(Ordering::Relaxed);
let mut note_lo = editor.note_lo.load(Ordering::Relaxed);
//if note_point < note_lo {
//note_lo = note_point;
//editor.note_lo.store(note_lo, Ordering::Relaxed);
//}
let mut note_hi = 127.min((note_lo + height).saturating_sub(2));
if note_point > note_hi {
note_lo += note_point - note_hi;
note_hi = note_point;
editor.note_lo.store(note_lo, Ordering::Relaxed);
}
Self {
focused: state.editor_focused(),
entered: state.editor_entered(),
note_len: editor.note_len,
phrase: &editor.phrase,
buffer: &editor.buffer,
now: &editor.now,
size: &editor.size,
view_mode: &editor.view_mode,
note_point,
note_range: (note_lo, note_hi),
note_names: (to_note_name(note_lo), to_note_name(note_hi)),
time_start: editor.time_start.load(Ordering::Relaxed),
time_point: editor.time_point.load(Ordering::Relaxed),
}
}
}
render!(|self: PhraseView<'a>|{
let Self {
focused, entered, size,
phrase, view_mode, buffer,
note_point, note_len,
note_range: (note_lo, note_hi),
note_names: (note_lo_name, note_hi_name),
time_start, time_point,
//now: _,
..
} = self;
lay!([
lay!(move|add|{
let title_color = if *focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)};
let upper_left = format!("{note_hi} {note_hi_name} {}",
phrase.as_ref().map(|p|p.read().unwrap().name.clone()).unwrap_or(String::new())
);
let lower_left = format!("{note_lo} {note_lo_name}");
let mut lower_right = format!(" {} ", size.format());
if *focused && *entered {
lower_right = format!("Note: {} ({}) {} {lower_right}",
note_point, to_note_name(*note_point), pulses_to_name(*note_len)
);
}
let mut upper_right = format!("[{}]", if *entered {""} else {" "});
if let Some(phrase) = phrase {
upper_right = format!("Time: {}/{} {} {upper_right}",
time_point, phrase.read().unwrap().length, pulses_to_name(view_mode.time_zoom()),
)
};
add(&Tui::at_nw(Tui::fg(title_color, upper_left)))?;
add(&Tui::at_sw(Tui::fg(title_color, lower_left)))?;
add(&Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right)))))?;
add(&Tui::fill_xy(Tui::at_se(Tui::pull_x(1, Tui::fg(title_color, lower_right)))))?;
Ok(())
}),
Tui::bg(Color::Rgb(40, 50, 30), Tui::fill_x(row!([
Tui::push_y(1, Tui::fill_y(Widget::new(|to:[u16;2]|Ok(Some(to.clip_w(2))), move|to: &mut TuiOutput|{
Ok(if to.area().h() >= 2 { view_mode.render_keys(to, *note_hi, *note_lo) })
}))),
Tui::fill_x(lay!([
Tui::push_y(1, Tui::fill_x(Widget::new(|to|Ok(Some(to)), |to: &mut TuiOutput|{
size.set_wh(to.area.w(), to.area.h() as usize - 1);
let draw = to.area().h() >= 2;
Ok(if draw { view_mode.render_notes(to, buffer, *time_start, *note_hi) })
}))),
Tui::push_y(1, Widget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
Ok(if *focused && *entered {
view_mode.render_cursor(
to,
*time_point, *time_start, view_mode.time_zoom(),
*note_point, *note_len, *note_hi, *note_lo,
)
})
}))
])),
])))
])
});
#[derive(Copy, Clone, Debug)]
pub enum PhraseViewNoteZoom {
N(usize),
Half,
Octant,
}
impl PhraseViewMode {
pub fn time_zoom (&self) -> usize {
match self {
Self::PianoHorizontal { time_zoom, .. } => *time_zoom,
_ => unimplemented!()
}
}
pub fn set_time_zoom (&mut self, time_zoom: usize) {
*self = match self {
Self::PianoHorizontal { note_zoom, .. } => Self::PianoHorizontal {
note_zoom: *note_zoom,
time_zoom,
},
_ => unimplemented!()
}
}
/// Return a new [BigBuffer] containing a render of the phrase.
pub fn draw (&self, phrase: &Phrase) -> BigBuffer {
let mut buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase));
match self {
Self::PianoHorizontal { time_zoom, note_zoom } => match note_zoom {
PhraseViewNoteZoom::N(_) => Self::draw_piano_horizontal(
&mut buffer, phrase, *time_zoom, 1
),
_ => unimplemented!(),
},
_ => unimplemented!(),
}
buffer
}
/// Draw a subsection of the [BigBuffer] onto a regular ratatui [Buffer].
fn render_notes (
&self,
target: &mut TuiOutput,
source: &BigBuffer,
time_start: usize,
note_hi: usize,
) {
let area = target.area();
let target = &mut target.buffer;
match self {
Self::PianoHorizontal { .. } => {
let [x0, y0, w, h] = area.xywh();
for (x, target_x) in (x0..x0+w).enumerate() {
for (y, target_y) in (y0..y0+h).enumerate() {
if y > note_hi {
break
}
let source_x = time_start + x;
let source_y = note_hi - y;
// TODO: enable loop rollover:
//let source_x = (time_start + x) % source.width.max(1);
//let source_y = (note_hi - y) % source.height.max(1);
if source_x < source.width && source_y < source.height {
let target_cell = target.get_mut(target_x, target_y);
if let Some(source_cell) = source.get(source_x, source_y) {
*target_cell = source_cell.clone();
}
}
}
}
},
_ => unimplemented!()
}
}
fn render_keys (&self, to: &mut TuiOutput, note_hi: usize, note_lo: usize) {
let style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0)));
match self {
Self::PianoHorizontal { .. } => {
let [x0, y0, _, _] = to.area().xywh();
for (y, note) in (note_lo..=note_hi).rev().enumerate() {
to.blit(&match note % 12 {
11 => "██",
10 => " ",
9 => "██",
8 => " ",
7 => "██",
6 => " ",
5 => "██",
4 => "██",
3 => " ",
2 => "██",
1 => " ",
0 => "██",
_ => unreachable!(),
}, x0, y0 + y as u16, style)
}
},
_ => unimplemented!()
}
}
fn render_cursor (
&self,
to: &mut TuiOutput,
time_point: usize,
time_start: usize,
time_zoom: usize,
note_point: usize,
note_len: usize,
note_hi: usize,
note_lo: usize,
) {
let style = Some(Style::default().fg(Color::Rgb(0,255,0)));
match self {
Self::PianoHorizontal { .. } => {
let [x0, y0, w, _] = to.area().xywh();
for (y, note) in (note_lo..=note_hi).rev().enumerate() {
if note == note_point {
for x in 0..w {
let time_1 = time_start + x as usize * time_zoom;
let time_2 = time_1 + time_zoom;
if time_1 <= time_point && time_point < time_2 {
to.blit(&"", x0 + x as u16, y0 + y as u16, style);
let tail = note_len as u16 / time_zoom as u16;
for x_tail in (x0 + x + 1)..(x0 + x + tail) {
to.blit(&"", x_tail, y0 + y as u16, style);
}
break
}
}
break
}
}
},
_ => unimplemented!()
}
}
/// Determine the required width to render the phrase.
fn buffer_width (&self, phrase: &Phrase) -> usize {
match self {
Self::PianoHorizontal { time_zoom, .. } => {
phrase.length / time_zoom
},
Self::PianoVertical { note_zoom, .. } => match note_zoom {
PhraseViewNoteZoom::Half => 64,
PhraseViewNoteZoom::N(n) => 128*n,
_ => unimplemented!()
},
}
}
/// Determine the required height to render the phrase.
fn buffer_height (&self, phrase: &Phrase) -> usize {
match self {
Self::PianoHorizontal { note_zoom, .. } => match note_zoom {
PhraseViewNoteZoom::Half => 64,
PhraseViewNoteZoom::N(n) => 128*n,
_ => unimplemented!()
},
Self::PianoVertical { time_zoom, .. } => {
phrase.length / time_zoom
},
}
}
/// Draw the piano roll using full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_piano_horizontal (
target: &mut BigBuffer, phrase: &Phrase, time_zoom: usize, _: usize
) {
let style = Style::default().fg(Color::Rgb(255, 255, 255));
for (y, note) in (0..127).rev().enumerate() {
for (x, time) in (0..target.width).map(|x|(x, x*time_zoom)) {
let cell = target.get_mut(x, y).unwrap();
cell.set_fg(Color::Rgb(48, 55, 45));
cell.set_char(if time % 384 == 0 {
'│'
} else if time % 96 == 0 {
'╎'
} else if note % 12 == 0 {
'='
} else {
'·'
});
}
}
let mut notes_on = [false;128];
for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() {
let time_end = time_start + time_zoom;
for (y, note) in (0..127).rev().enumerate() {
let cell = target.get_mut(x, note).unwrap();
if notes_on[note] {
cell.set_fg(Color::Rgb(255, 255, 255));
cell.set_bg(Color::Rgb(0, 0, 0));
cell.set_char('▄');
cell.set_style(style);
}
}
for time in time_start..time_end {
for event in phrase.notes[time].iter() {
match event {
MidiMessage::NoteOn { key, .. } => {
let note = key.as_int() as usize;
let cell = target.get_mut(x, note).unwrap();
cell.set_char('█');
cell.set_style(style);
notes_on[note] = true
},
MidiMessage::NoteOff { key, .. } => {
notes_on[key.as_int() as usize] = false
},
_ => {}
}
}
}
}
}
/// TODO: Draw the piano roll using octant blocks (U+1CD00-U+1CDE5)
fn draw_piano_horizontal_octant (
_: &mut BigBuffer, _: &Phrase, _: usize
) {
unimplemented!()
}
/// TODO: Draw the piano roll using half blocks: ▄▀▄
fn draw_piano_horizontal_half (
_: &mut BigBuffer, _: &Phrase, _: usize
) {
unimplemented!()
}
}
//impl PhraseEditorModel {
//pub(crate) fn redraw (phrase: &Phrase, mode: PhraseViewMode) -> BigBuffer {
//let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65);
//Self::fill_seq_bg(mode, &mut buf, phrase.length, phrase.ppq);
//Self::fill_seq_fg(mode, &mut buf, &phrase);
//buf
//}
//fn fill_seq_bg (_mode: PhraseViewMode, buf: &mut BigBuffer, length: usize, ppq: usize) {
//for x in 0..buf.width {
//// Only fill as far as phrase length
//if x as usize >= length { break }
//// Fill each row with background characters
//for y in 0..buf.height {
//if y >= 64 {
//break
//}
//buf.get_mut(x, y).map(|cell|{
//cell.set_char(if ppq == 0 {
//'·'
//} else if x % (4 * ppq) == 0 {
//'│'
//} else if x % ppq == 0 {
//'╎'
//} else {
//'·'
//});
//cell.set_fg(Color::Rgb(48, 64, 56));
//cell.modifier = Modifier::DIM;
//});
//}
//}
//}
//fn fill_seq_fg (mode: PhraseViewMode, buf: &mut BigBuffer, phrase: &Phrase) {
//match mode {
//PhraseViewMode::Horizontal =>
//Self::fill_seq_fg_horizontal(buf, phrase),
//PhraseViewMode::HorizontalHalf =>
//Self::fill_seq_fg_horizontal_half(buf, phrase),
//PhraseViewMode::Vertical =>
//Self::fill_seq_fg_vertical(buf, phrase),
//}
//}
//fn fill_seq_fg_horizontal (buf: &mut BigBuffer, phrase: &Phrase) {
//let mut notes_on = [false;128];
//for x in 0..buf.width {
//}
//}
//fn fill_seq_fg_horizontal_half (buf: &mut BigBuffer, phrase: &Phrase) {
//let mut notes_on = [false;128];
//for x in 0..buf.width {
//if x as usize >= phrase.length {
//break
//}
//if let Some(notes) = phrase.notes.get(x as usize) {
//if phrase.percussive {
//for note in notes {
//match note {
//MidiMessage::NoteOn { key, .. } =>
//notes_on[key.as_int() as usize] = true,
//_ => {}
//}
//}
//} else {
//for note in notes {
//match note {
//MidiMessage::NoteOn { key, .. } =>
//notes_on[key.as_int() as usize] = true,
//MidiMessage::NoteOff { key, .. } =>
//notes_on[key.as_int() as usize] = false,
//_ => {}
//}
//}
//}
//for y in 0..buf.height {
//if y > 63 {
//break
//}
//let y = 63 - y;
//if let Some(block) = half_block(
//notes_on[y as usize * 2 + 1],
//notes_on[y as usize * 2],
//) {
//buf.get_mut(x, y).map(|cell|{
//cell.set_char(block);
//cell.set_fg(Color::White);
//});
//}
//}
//if phrase.percussive {
//notes_on.fill(false);
//}
//}
//}
//}
//}
////const NTH_OCTAVE: [&'static str; 11] = [
////"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "X",
//////"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8",
////];
#[derive(Clone, Debug)]
pub enum PhraseCommand {
// TODO: 1-9 seek markers that by default start every 8th of the phrase
AppendNote,
PutNote,
SetNoteCursor(usize),
SetNoteLength(usize),
SetNoteScroll(usize),
SetTimeCursor(usize),
SetTimeScroll(usize),
SetTimeZoom(usize),
Show(Option<Arc<RwLock<Phrase>>>),
SetEditMode(PhraseEditMode),
ToggleDirection,
}
impl InputToCommand<Tui, PhraseEditorModel> for PhraseCommand {
fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option<Self> {
use PhraseCommand::*;
use KeyCode::{Char, Esc, Up, Down, PageUp, PageDown, Left, Right};
let note_lo = state.note_lo.load(Ordering::Relaxed);
let note_point = state.note_point.load(Ordering::Relaxed);
let time_start = state.time_start.load(Ordering::Relaxed);
let time_point = state.time_point.load(Ordering::Relaxed);
let time_zoom = state.view_mode.time_zoom();
let length = state.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
Some(match from.event() {
key!(Char('`')) => ToggleDirection,
key!(Esc) => SetEditMode(PhraseEditMode::Scroll),
key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)),
key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)),
key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)),
key!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)),
key!(Char('a')) => AppendNote,
key!(Char('s')) => PutNote,
key!(Char('[')) => SetNoteLength(prev_note_length(state.note_len)),
key!(Char(']')) => SetNoteLength(next_note_length(state.note_len)),
key!(Char('n')) => { todo!("toggle keys vs notes") },
_ => match state.edit_mode {
PhraseEditMode::Scroll => match from.event() {
key!(Char('e')) => SetEditMode(PhraseEditMode::Note),
key!(Up) => SetNoteScroll(note_lo + 1),
key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)),
key!(PageUp) => SetNoteScroll(note_lo + 3),
key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)),
key!(Left) => SetTimeScroll(time_start.saturating_sub(1)),
key!(Right) => SetTimeScroll(time_start + 1),
_ => return None
},
PhraseEditMode::Note => match from.event() {
key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll),
key!(Up) => SetNoteCursor(note_point + 1),
key!(Down) => SetNoteCursor(note_point.saturating_sub(1)),
key!(PageUp) => SetNoteCursor(note_point + 3),
key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)),
key!(Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)),
key!(Right) => SetTimeCursor((time_point + time_zoom) % length),
_ => return None
},
}
})
}
}
impl Command<PhraseEditorModel> for PhraseCommand {
fn execute (self, state: &mut PhraseEditorModel) -> Perhaps<Self> {
use PhraseCommand::*;
Ok(match self {
Show(phrase) => {
state.show_phrase(phrase);
None
},
ToggleDirection => {
todo!()
},
SetEditMode(mode) => {
state.edit_mode = mode;
None
}
AppendNote => {
state.put_note();
state.time_cursor_advance();
None
},
PutNote => {
state.put_note();
None
},
SetTimeCursor(time) => {
state.time_point.store(time, Ordering::Relaxed);
None
},
SetTimeScroll(time) => {
state.time_start.store(time, Ordering::Relaxed);
None
},
SetTimeZoom(zoom) => {
state.view_mode.set_time_zoom(zoom);
state.show_phrase(state.phrase.clone());
None
},
SetNoteScroll(note) => {
state.note_lo.store(note, Ordering::Relaxed);
None
},
SetNoteLength(time) => {
state.note_len = time;
None
},
SetNoteCursor(note) => {
let note = 127.min(note);
let start = state.note_lo.load(Ordering::Relaxed);
state.note_point.store(note, Ordering::Relaxed);
if note < start {
state.note_lo.store(note, Ordering::Relaxed);
}
None
},
})
}
}