mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 12:16:42 +01:00
extract piano_horizontal
This commit is contained in:
parent
69faadac2b
commit
09a7d17121
11 changed files with 470 additions and 483 deletions
|
|
@ -3,7 +3,6 @@ use crate::*;
|
||||||
mod phrase; pub(crate) use phrase::*;
|
mod phrase; pub(crate) use phrase::*;
|
||||||
mod jack; pub(crate) use self::jack::*;
|
mod jack; pub(crate) use self::jack::*;
|
||||||
mod clip; pub(crate) use clip::*;
|
mod clip; pub(crate) use clip::*;
|
||||||
mod color; pub(crate) use color::*;
|
|
||||||
mod clock; pub(crate) use clock::*;
|
mod clock; pub(crate) use clock::*;
|
||||||
mod player; pub(crate) use player::*;
|
mod player; pub(crate) use player::*;
|
||||||
mod scene; pub(crate) use scene::*;
|
mod scene; pub(crate) use scene::*;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
pub trait HasColor {
|
|
||||||
fn color (&self) -> ItemColor;
|
|
||||||
fn color_mut (&self) -> &mut ItemColor;
|
|
||||||
}
|
|
||||||
|
|
@ -25,7 +25,8 @@ pub trait HasPlayPhrase: HasClock {
|
||||||
fn pulses_since_start_looped (&self) -> Option<f64> {
|
fn pulses_since_start_looped (&self) -> Option<f64> {
|
||||||
if let Some((started, Some(phrase))) = self.play_phrase().as_ref() {
|
if let Some((started, Some(phrase))) = self.play_phrase().as_ref() {
|
||||||
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
||||||
let elapsed = (elapsed as usize % phrase.read().unwrap().length) as f64;
|
let length = phrase.read().unwrap().length.max(1); // prevent div0 on empty phrase
|
||||||
|
let elapsed = (elapsed as usize % length) as f64;
|
||||||
Some(elapsed)
|
Some(elapsed)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ use crate::*;
|
||||||
use rand::{thread_rng, distributions::uniform::UniformSampler};
|
use rand::{thread_rng, distributions::uniform::UniformSampler};
|
||||||
pub use ratatui::prelude::Color;
|
pub use ratatui::prelude::Color;
|
||||||
|
|
||||||
|
pub trait HasColor {
|
||||||
|
fn color (&self) -> ItemColor;
|
||||||
|
fn color_mut (&self) -> &mut ItemColor;
|
||||||
|
}
|
||||||
|
|
||||||
/// A color in OKHSL and RGB representations.
|
/// A color in OKHSL and RGB representations.
|
||||||
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
||||||
pub struct ItemColor {
|
pub struct ItemColor {
|
||||||
|
|
@ -48,15 +53,19 @@ impl ItemColor {
|
||||||
self.okhsl.mix(other.okhsl, distance).into()
|
self.okhsl.mix(other.okhsl, distance).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl From<Color> for ItemPalette {
|
||||||
|
fn from (base: Color) -> Self {
|
||||||
|
Self::from(ItemColor::from(base))
|
||||||
|
}
|
||||||
|
}
|
||||||
impl From<ItemColor> for ItemPalette {
|
impl From<ItemColor> for ItemPalette {
|
||||||
|
|
||||||
fn from (base: ItemColor) -> Self {
|
fn from (base: ItemColor) -> Self {
|
||||||
let mut light = base.okhsl.clone();
|
let mut light = base.okhsl.clone();
|
||||||
light.lightness = (light.lightness * 1.2).min(Okhsl::<f32>::max_lightness());
|
light.lightness = (light.lightness * 1.33).min(Okhsl::<f32>::max_lightness());
|
||||||
let mut lighter = light.clone();
|
let mut lighter = light.clone();
|
||||||
lighter.lightness = (lighter.lightness * 1.2).min(Okhsl::<f32>::max_lightness());
|
lighter.lightness = (lighter.lightness * 1.33).min(Okhsl::<f32>::max_lightness());
|
||||||
let mut lightest = lighter.clone();
|
let mut lightest = lighter.clone();
|
||||||
lightest.lightness = (lightest.lightness * 1.2).min(Okhsl::<f32>::max_lightness());
|
lightest.lightness = (lightest.lightness * 1.33).min(Okhsl::<f32>::max_lightness());
|
||||||
|
|
||||||
let mut dark = base.okhsl.clone();
|
let mut dark = base.okhsl.clone();
|
||||||
dark.lightness = (dark.lightness * 0.75).max(Okhsl::<f32>::min_lightness());
|
dark.lightness = (dark.lightness * 0.75).max(Okhsl::<f32>::min_lightness());
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ mod engine_output; pub(crate) use engine_output::*;
|
||||||
|
|
||||||
mod app_transport; pub(crate) use app_transport::*;
|
mod app_transport; pub(crate) use app_transport::*;
|
||||||
mod app_sequencer; pub(crate) use app_sequencer::*;
|
mod app_sequencer; pub(crate) use app_sequencer::*;
|
||||||
|
mod app_groovebox; pub(crate) use app_groovebox::*;
|
||||||
mod app_arranger; pub(crate) use app_arranger::*;
|
mod app_arranger; pub(crate) use app_arranger::*;
|
||||||
|
|
||||||
////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////
|
||||||
|
|
@ -16,6 +17,7 @@ mod app_arranger; pub(crate) use app_arranger::*;
|
||||||
mod status_bar; pub(crate) use status_bar::*;
|
mod status_bar; pub(crate) use status_bar::*;
|
||||||
mod file_browser; pub(crate) use file_browser::*;
|
mod file_browser; pub(crate) use file_browser::*;
|
||||||
mod phrase_editor; pub(crate) use phrase_editor::*;
|
mod phrase_editor; pub(crate) use phrase_editor::*;
|
||||||
|
mod piano_horizontal; pub(crate) use piano_horizontal::*;
|
||||||
mod phrase_length; pub(crate) use phrase_length::*;
|
mod phrase_length; pub(crate) use phrase_length::*;
|
||||||
mod phrase_rename; pub(crate) use phrase_rename::*;
|
mod phrase_rename; pub(crate) use phrase_rename::*;
|
||||||
mod phrase_list; pub(crate) use phrase_list::*;
|
mod phrase_list; pub(crate) use phrase_list::*;
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ impl Audio for SequencerTui {
|
||||||
|
|
||||||
render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1,
|
render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1,
|
||||||
Tui::fill_xy(SequencerStatusBar::from(self)),
|
Tui::fill_xy(SequencerStatusBar::from(self)),
|
||||||
Tui::split_right(false, 20,
|
Tui::split_right(false, if self.size.w() > 60 { 20 } else if self.size.w() > 40 { 15 } else { 10 },
|
||||||
Tui::fixed_x(20, Tui::split_down(false, 4, col!([
|
Tui::fixed_x(20, Tui::split_down(false, 4, col!([
|
||||||
PhraseSelector::play_phrase(&self.player),
|
PhraseSelector::play_phrase(&self.player),
|
||||||
PhraseSelector::next_phrase(&self.player),
|
PhraseSelector::next_phrase(&self.player),
|
||||||
|
|
@ -273,6 +273,10 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option<S
|
||||||
PhraseEditMode::Note => PhraseEditMode::Scroll,
|
PhraseEditMode::Note => PhraseEditMode::Scroll,
|
||||||
})),
|
})),
|
||||||
|
|
||||||
|
// Enqueue currently edited phrase
|
||||||
|
key!(Char('q')) =>
|
||||||
|
Enqueue(Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())),
|
||||||
|
|
||||||
// E: Toggle between editing currently playing or other phrase
|
// E: Toggle between editing currently playing or other phrase
|
||||||
key!(Char('e')) => if let Some((_, Some(playing_phrase))) = state.player.play_phrase() {
|
key!(Char('e')) => if let Some((_, Some(playing_phrase))) = state.player.play_phrase() {
|
||||||
let editing_phrase = state.editor.phrase.as_ref().map(|p|p.read().unwrap().clone());
|
let editing_phrase = state.editor.phrase.as_ref().map(|p|p.read().unwrap().clone());
|
||||||
|
|
@ -302,10 +306,6 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option<S
|
||||||
key!(Char('[')) | key!(Char(']')) | key!(Char('c')) | key!(Shift-Char('A')) | key!(Shift-Char('D')) =>
|
key!(Char('[')) | key!(Char(']')) | key!(Char('c')) | key!(Shift-Char('A')) | key!(Shift-Char('D')) =>
|
||||||
Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?),
|
Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?),
|
||||||
|
|
||||||
// Enqueue currently edited phrase
|
|
||||||
key!(Enter) =>
|
|
||||||
Enqueue(Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())),
|
|
||||||
|
|
||||||
// Delegate to focused control:
|
// Delegate to focused control:
|
||||||
_ => match state.focus {
|
_ => match state.focus {
|
||||||
PhraseEditor => Editor(PhraseCommand::input_to_command(&state.editor, input)?),
|
PhraseEditor => Editor(PhraseCommand::input_to_command(&state.editor, input)?),
|
||||||
|
|
|
||||||
|
|
@ -10,25 +10,54 @@ pub trait HasEditor {
|
||||||
pub struct PhraseEditorModel {
|
pub struct PhraseEditorModel {
|
||||||
/// Phrase being played
|
/// Phrase being played
|
||||||
pub(crate) phrase: Option<Arc<RwLock<Phrase>>>,
|
pub(crate) phrase: Option<Arc<RwLock<Phrase>>>,
|
||||||
|
pub(crate) view_mode: Box<dyn PhraseViewMode>,
|
||||||
|
pub(crate) edit_mode: PhraseEditMode,
|
||||||
|
// Lowest note displayed
|
||||||
|
pub(crate) note_lo: AtomicUsize,
|
||||||
|
/// Note coordinate of cursor
|
||||||
|
pub(crate) note_point: AtomicUsize,
|
||||||
/// Length of note that will be inserted, in pulses
|
/// Length of note that will be inserted, in pulses
|
||||||
pub(crate) note_len: usize,
|
pub(crate) note_len: usize,
|
||||||
/// Notes currently held at input
|
/// Notes currently held at input
|
||||||
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
|
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
|
||||||
/// Notes currently held at output
|
/// Notes currently held at output
|
||||||
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
|
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
|
||||||
|
/// Earliest time displayed
|
||||||
|
pub(crate) time_start: AtomicUsize,
|
||||||
|
/// Time coordinate of cursor
|
||||||
|
pub(crate) time_point: AtomicUsize,
|
||||||
/// Current position of global playhead
|
/// Current position of global playhead
|
||||||
pub(crate) now: Arc<Pulse>,
|
pub(crate) now: Arc<Pulse>,
|
||||||
/// Width and height of notes area at last render
|
/// Width and height of notes area at last render
|
||||||
pub(crate) size: Measure<Tui>,
|
pub(crate) size: Measure<Tui>,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) note_lo: AtomicUsize,
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub(crate) note_point: AtomicUsize,
|
pub enum PhraseEditMode {
|
||||||
|
Note,
|
||||||
|
Scroll,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) time_start: AtomicUsize,
|
pub trait PhraseViewMode: Debug + Send + Sync {
|
||||||
pub(crate) time_point: AtomicUsize,
|
fn show (&mut self, phrase: Option<&Phrase>, note_len: usize);
|
||||||
|
fn time_zoom (&self) -> Option<usize>;
|
||||||
pub(crate) edit_mode: PhraseEditMode,
|
fn set_time_zoom (&mut self, time_zoom: Option<usize>);
|
||||||
pub(crate) view_mode: Box<dyn PhraseViewMode + Send + Sync>,
|
fn buffer_width (&self, phrase: &Phrase) -> usize;
|
||||||
|
fn buffer_height (&self, phrase: &Phrase) -> usize;
|
||||||
|
fn render_keys (&self,
|
||||||
|
to: &mut TuiOutput, color: Color, point: Option<usize>, range: (usize, usize));
|
||||||
|
fn render_notes (&self,
|
||||||
|
to: &mut TuiOutput, time_start: usize, note_hi: usize);
|
||||||
|
fn render_cursor (
|
||||||
|
&self,
|
||||||
|
to: &mut TuiOutput,
|
||||||
|
time_point: usize,
|
||||||
|
time_start: usize,
|
||||||
|
note_point: usize,
|
||||||
|
note_len: usize,
|
||||||
|
note_hi: usize,
|
||||||
|
note_lo: usize,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for PhraseEditorModel {
|
impl std::fmt::Debug for PhraseEditorModel {
|
||||||
|
|
@ -105,6 +134,121 @@ impl PhraseEditorModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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(Option<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);
|
||||||
|
let note_len = state.note_len;
|
||||||
|
Some(match from.event() {
|
||||||
|
key!(Char('`')) => ToggleDirection,
|
||||||
|
key!(Esc) => SetEditMode(PhraseEditMode::Scroll),
|
||||||
|
key!(Char('z')) => SetTimeZoom(None),
|
||||||
|
key!(Char('-')) => SetTimeZoom(time_zoom.map(next_note_length)),
|
||||||
|
key!(Char('_')) => SetTimeZoom(time_zoom.map(next_note_length)),
|
||||||
|
key!(Char('=')) => SetTimeZoom(time_zoom.map(prev_note_length)),
|
||||||
|
key!(Char('+')) => SetTimeZoom(time_zoom.map(prev_note_length)),
|
||||||
|
key!(Char('a')) => AppendNote,
|
||||||
|
key!(Char('s')) => PutNote,
|
||||||
|
// TODO: no triplet/dotted
|
||||||
|
key!(Char(',')) => SetNoteLength(prev_note_length(state.note_len)),
|
||||||
|
key!(Char('.')) => SetNoteLength(next_note_length(state.note_len)),
|
||||||
|
// TODO: with triplet/dotted
|
||||||
|
key!(Char('<')) => SetNoteLength(prev_note_length(state.note_len)),
|
||||||
|
key!(Char('>')) => SetNoteLength(next_note_length(state.note_len)),
|
||||||
|
// TODO: '/' set triplet, '?' set dotted
|
||||||
|
_ => 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(note_len)),
|
||||||
|
key!(Right) => SetTimeScroll(time_start + note_len),
|
||||||
|
key!(Shift-Left) => SetTimeScroll(time_point.saturating_sub(time_zoom.unwrap())),
|
||||||
|
key!(Shift-Right) => SetTimeScroll((time_point + time_zoom.unwrap()) % length),
|
||||||
|
_ => 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!(Shift-Up) => SetNoteScroll(note_lo + 1),
|
||||||
|
key!(Shift-Down) => SetNoteScroll(note_lo.saturating_sub(1)),
|
||||||
|
key!(Shift-PageUp) => SetNoteScroll(note_point + 3),
|
||||||
|
key!(Shift-PageDown) => SetNoteScroll(note_point.saturating_sub(3)),
|
||||||
|
|
||||||
|
key!(Left) => SetTimeCursor(time_point.saturating_sub(note_len)),
|
||||||
|
key!(Right) => SetTimeCursor((time_point + note_len) % length),
|
||||||
|
|
||||||
|
key!(Shift-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom.unwrap())),
|
||||||
|
key!(Shift-Right) => SetTimeCursor((time_point + time_zoom.unwrap()) % length),
|
||||||
|
|
||||||
|
_ => return None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command<PhraseEditorModel> for PhraseCommand {
|
||||||
|
fn execute (self, state: &mut PhraseEditorModel) -> Perhaps<Self> {
|
||||||
|
use PhraseCommand::*;
|
||||||
|
match self {
|
||||||
|
Show(phrase) => { state.show_phrase(phrase); },
|
||||||
|
SetEditMode(mode) => { state.edit_mode = mode; }
|
||||||
|
PutNote => { state.put_note(); },
|
||||||
|
AppendNote => { state.put_note();
|
||||||
|
state.time_cursor_advance(); },
|
||||||
|
SetTimeZoom(zoom) => { state.view_mode.set_time_zoom(zoom);
|
||||||
|
state.show_phrase(state.phrase.clone()); },
|
||||||
|
SetTimeScroll(time) => { state.time_start.store(time, Ordering::Relaxed); },
|
||||||
|
SetTimeCursor(time) => { state.time_point.store(time, Ordering::Relaxed); },
|
||||||
|
SetNoteLength(time) => { state.note_len = time; },
|
||||||
|
SetNoteScroll(note) => { state.note_lo.store(note, Ordering::Relaxed); },
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => todo!("{:?}", self)
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PhraseView<'a> {
|
pub struct PhraseView<'a> {
|
||||||
note_point: usize,
|
note_point: usize,
|
||||||
note_range: (usize, usize),
|
note_range: (usize, usize),
|
||||||
|
|
@ -112,22 +256,30 @@ pub struct PhraseView<'a> {
|
||||||
time_point: usize,
|
time_point: usize,
|
||||||
note_len: usize,
|
note_len: usize,
|
||||||
phrase: &'a Option<Arc<RwLock<Phrase>>>,
|
phrase: &'a Option<Arc<RwLock<Phrase>>>,
|
||||||
view_mode: &'a Box<dyn PhraseViewMode + Send + Sync>,
|
view_mode: &'a Box<dyn PhraseViewMode>,
|
||||||
now: &'a Arc<Pulse>,
|
now: &'a Arc<Pulse>,
|
||||||
size: &'a Measure<Tui>,
|
size: &'a Measure<Tui>,
|
||||||
focused: bool,
|
focused: bool,
|
||||||
entered: bool,
|
entered: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
render!(|self: PhraseView<'a>|{
|
render!(|self: PhraseView<'a>|{
|
||||||
let bg = if self.focused { TuiTheme::g(32) } else { Color::Reset };
|
let bg = if self.focused { TuiTheme::g(32) } else { Color::Reset };
|
||||||
let fg = self.phrase.as_ref()
|
let fg = self.phrase.as_ref()
|
||||||
.map(|p|p.read().unwrap().color.clone())
|
.map(|p|p.read().unwrap().color.clone())
|
||||||
.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64))));
|
.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64))));
|
||||||
Tui::bg(bg, Tui::split_up(false, 2,
|
Tui::bg(bg, Tui::split_up(false, 2,
|
||||||
Tui::bg(fg.dark.rgb, lay!([PhraseTimeline(&self, fg), PhraseViewStats(&self, fg),])),
|
Tui::bg(fg.dark.rgb, lay!([
|
||||||
Split::right(false, 5, PhraseKeys(&self, fg), lay!([PhraseNotes(&self, fg), PhraseCursor(&self), ])),
|
PhraseTimeline(&self, fg),
|
||||||
|
PhraseViewStats(&self, fg),
|
||||||
|
])),
|
||||||
|
Split::right(false, 5, PhraseKeys(&self, fg), lay!([
|
||||||
|
PhraseNotes(&self, fg),
|
||||||
|
PhraseCursor(&self),
|
||||||
|
])),
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
|
|
||||||
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 editor = state.editor();
|
let editor = state.editor();
|
||||||
|
|
@ -211,374 +363,3 @@ render!(|self: PhraseCursor<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|Ok(
|
||||||
self.0.note_range.0,
|
self.0.note_range.0,
|
||||||
)
|
)
|
||||||
))));
|
))));
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub enum PhraseEditMode {
|
|
||||||
Note,
|
|
||||||
Scroll,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PhraseViewMode {
|
|
||||||
fn show (&mut self, phrase: Option<&Phrase>, note_len: usize);
|
|
||||||
fn time_zoom (&self) -> Option<usize>;
|
|
||||||
fn set_time_zoom (&mut self, time_zoom: Option<usize>);
|
|
||||||
fn buffer_width (&self, phrase: &Phrase) -> usize;
|
|
||||||
fn buffer_height (&self, phrase: &Phrase) -> usize;
|
|
||||||
fn render_keys (&self,
|
|
||||||
to: &mut TuiOutput, color: Color, point: Option<usize>, range: (usize, usize));
|
|
||||||
fn render_notes (&self,
|
|
||||||
to: &mut TuiOutput, time_start: usize, note_hi: usize);
|
|
||||||
fn render_cursor (
|
|
||||||
&self,
|
|
||||||
to: &mut TuiOutput,
|
|
||||||
time_point: usize,
|
|
||||||
time_start: usize,
|
|
||||||
note_point: usize,
|
|
||||||
note_len: usize,
|
|
||||||
note_hi: usize,
|
|
||||||
note_lo: usize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PianoHorizontal {
|
|
||||||
time_zoom: Option<usize>,
|
|
||||||
note_zoom: PhraseViewNoteZoom,
|
|
||||||
buffer: BigBuffer,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub enum PhraseViewNoteZoom {
|
|
||||||
N(usize),
|
|
||||||
Half,
|
|
||||||
Octant,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PhraseViewMode for PianoHorizontal {
|
|
||||||
fn time_zoom (&self) -> Option<usize> {
|
|
||||||
self.time_zoom
|
|
||||||
}
|
|
||||||
fn set_time_zoom (&mut self, time_zoom: Option<usize>) {
|
|
||||||
self.time_zoom = time_zoom
|
|
||||||
}
|
|
||||||
fn show (&mut self, phrase: Option<&Phrase>, note_len: usize) {
|
|
||||||
if let Some(phrase) = phrase {
|
|
||||||
self.buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase));
|
|
||||||
draw_piano_horizontal_bg(&mut self.buffer, phrase, self.time_zoom.unwrap(), note_len);
|
|
||||||
draw_piano_horizontal_fg(&mut self.buffer, phrase, self.time_zoom.unwrap());
|
|
||||||
} else {
|
|
||||||
self.buffer = Default::default();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn buffer_width (&self, phrase: &Phrase) -> usize {
|
|
||||||
phrase.length / self.time_zoom.unwrap()
|
|
||||||
}
|
|
||||||
/// Determine the required height to render the phrase.
|
|
||||||
fn buffer_height (&self, phrase: &Phrase) -> usize {
|
|
||||||
match self.note_zoom {
|
|
||||||
PhraseViewNoteZoom::Half => 64,
|
|
||||||
PhraseViewNoteZoom::N(n) => 128*n,
|
|
||||||
_ => unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn render_notes (
|
|
||||||
&self,
|
|
||||||
target: &mut TuiOutput,
|
|
||||||
time_start: usize,
|
|
||||||
note_hi: usize,
|
|
||||||
) {
|
|
||||||
let [x0, y0, w, h] = target.area().xywh();
|
|
||||||
let source = &self.buffer;
|
|
||||||
let target = &mut target.buffer;
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn render_keys (
|
|
||||||
&self,
|
|
||||||
to: &mut TuiOutput,
|
|
||||||
color: Color,
|
|
||||||
point: Option<usize>,
|
|
||||||
(note_lo, note_hi): (usize, usize)
|
|
||||||
) {
|
|
||||||
let [x, y0, _, _] = to.area().xywh();
|
|
||||||
let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0)));
|
|
||||||
let note_off_style = Some(Style::default().fg(TuiTheme::g(160)));
|
|
||||||
let note_on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(color).bold());
|
|
||||||
for (y, note) in (note_lo..=note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) {
|
|
||||||
let key = match note % 12 {
|
|
||||||
11 => "████▌",
|
|
||||||
10 => " ",
|
|
||||||
9 => "████▌",
|
|
||||||
8 => " ",
|
|
||||||
7 => "████▌",
|
|
||||||
6 => " ",
|
|
||||||
5 => "████▌",
|
|
||||||
4 => "████▌",
|
|
||||||
3 => " ",
|
|
||||||
2 => "████▌",
|
|
||||||
1 => " ",
|
|
||||||
0 => "████▌",
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
to.blit(&key, x, y, key_style);
|
|
||||||
|
|
||||||
if Some(note) == point {
|
|
||||||
to.blit(&format!("{:<5}", to_note_name(note)), x, y, note_on_style)
|
|
||||||
} else {
|
|
||||||
to.blit(&to_note_name(note), x, y, note_off_style)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn render_cursor (
|
|
||||||
&self,
|
|
||||||
to: &mut TuiOutput,
|
|
||||||
time_point: usize,
|
|
||||||
time_start: usize,
|
|
||||||
note_point: usize,
|
|
||||||
note_len: usize,
|
|
||||||
note_hi: usize,
|
|
||||||
note_lo: usize,
|
|
||||||
) {
|
|
||||||
let time_zoom = self.time_zoom.unwrap();
|
|
||||||
let [x0, y0, w, _] = to.area().xywh();
|
|
||||||
let style = Some(Style::default().fg(Color::Rgb(0,255,0)));
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
note_len: usize,
|
|
||||||
) {
|
|
||||||
draw_piano_horizontal_bg(target, phrase, time_zoom, note_len);
|
|
||||||
draw_piano_horizontal_fg(target, phrase, time_zoom);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_piano_horizontal_bg (
|
|
||||||
target: &mut BigBuffer,
|
|
||||||
phrase: &Phrase,
|
|
||||||
time_zoom: usize,
|
|
||||||
note_len: usize,
|
|
||||||
) {
|
|
||||||
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_bg(phrase.color.darkest.rgb);
|
|
||||||
cell.set_fg(phrase.color.darker.rgb);
|
|
||||||
cell.set_char(if time % 384 == 0 {
|
|
||||||
'│'
|
|
||||||
} else if time % 96 == 0 {
|
|
||||||
'╎'
|
|
||||||
} else if time % note_len == 0 {
|
|
||||||
'┊'
|
|
||||||
} else if (127 - note) % 12 == 1 {
|
|
||||||
'='
|
|
||||||
} else {
|
|
||||||
'·'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_piano_horizontal_fg (
|
|
||||||
target: &mut BigBuffer,
|
|
||||||
phrase: &Phrase,
|
|
||||||
time_zoom: usize,
|
|
||||||
) {
|
|
||||||
let style = Style::default().fg(phrase.color.lightest.rgb);//.bg(Color::Rgb(0, 0, 0));
|
|
||||||
let mut notes_on = [false;128];
|
|
||||||
for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() {
|
|
||||||
|
|
||||||
for (y, note) in (0..127).rev().enumerate() {
|
|
||||||
let cell = target.get_mut(x, note).unwrap();
|
|
||||||
if notes_on[note] {
|
|
||||||
cell.set_char('▂');
|
|
||||||
cell.set_style(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let time_end = time_start + time_zoom;
|
|
||||||
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
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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(Option<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);
|
|
||||||
let note_len = state.note_len;
|
|
||||||
Some(match from.event() {
|
|
||||||
key!(Char('`')) => ToggleDirection,
|
|
||||||
key!(Esc) => SetEditMode(PhraseEditMode::Scroll),
|
|
||||||
key!(Char('z')) => SetTimeZoom(None),
|
|
||||||
key!(Char('-')) => SetTimeZoom(time_zoom.map(next_note_length)),
|
|
||||||
key!(Char('_')) => SetTimeZoom(time_zoom.map(next_note_length)),
|
|
||||||
key!(Char('=')) => SetTimeZoom(time_zoom.map(prev_note_length)),
|
|
||||||
key!(Char('+')) => SetTimeZoom(time_zoom.map(prev_note_length)),
|
|
||||||
key!(Char('a')) => AppendNote,
|
|
||||||
key!(Char('s')) => PutNote,
|
|
||||||
// TODO: no triplet/dotted
|
|
||||||
key!(Char(',')) => SetNoteLength(prev_note_length(state.note_len)),
|
|
||||||
key!(Char('.')) => SetNoteLength(next_note_length(state.note_len)),
|
|
||||||
// TODO: with triplet/dotted
|
|
||||||
key!(Char('<')) => SetNoteLength(prev_note_length(state.note_len)),
|
|
||||||
key!(Char('>')) => SetNoteLength(next_note_length(state.note_len)),
|
|
||||||
// TODO: '/' set triplet, '?' set dotted
|
|
||||||
_ => 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(time_zoom.unwrap())),
|
|
||||||
key!(Right) => SetTimeScroll(time_start + time_zoom.unwrap()),
|
|
||||||
_ => 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(note_len)),
|
|
||||||
key!(Right) => SetTimeCursor((time_point + note_len) % length),
|
|
||||||
key!(Shift-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom.unwrap())),
|
|
||||||
key!(Shift-Right) => SetTimeCursor((time_point + time_zoom.unwrap()) % 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
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
tui::phrase_rename::PhraseRenameCommand as Rename,
|
||||||
|
tui::phrase_length::PhraseLengthCommand as Length,
|
||||||
|
tui::file_browser::FileBrowserCommand as Browse,
|
||||||
api::PhrasePoolCommand as Pool,
|
api::PhrasePoolCommand as Pool,
|
||||||
tui::{
|
|
||||||
phrase_rename::PhraseRenameCommand as Rename,
|
|
||||||
phrase_length::PhraseLengthCommand as Length,
|
|
||||||
file_browser::FileBrowserCommand as Browse,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -44,10 +42,22 @@ impl Default for PhraseListModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PhraseListModel {
|
impl HasPhrases for PhraseListModel {
|
||||||
pub(crate) fn phrase (&self) -> &Arc<RwLock<Phrase>> {
|
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
||||||
|
&self.phrases
|
||||||
|
}
|
||||||
|
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
|
||||||
|
&mut self.phrases
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasPhrase for PhraseListModel {
|
||||||
|
fn phrase (&self) -> &Arc<RwLock<Phrase>> {
|
||||||
&self.phrases[self.phrase_index()]
|
&self.phrases[self.phrase_index()]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PhraseListModel {
|
||||||
pub(crate) fn phrase_index (&self) -> usize {
|
pub(crate) fn phrase_index (&self) -> usize {
|
||||||
self.phrase.load(Ordering::Relaxed)
|
self.phrase.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
@ -142,11 +152,17 @@ render!(|self: PhraseListView<'a>|{
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub enum PhrasesCommand {
|
pub enum PhrasesCommand {
|
||||||
Select(usize),
|
/// Update the contents of the phrase pool
|
||||||
Phrase(Pool),
|
Phrase(Pool),
|
||||||
|
/// Select a phrase from the phrase pool
|
||||||
|
Select(usize),
|
||||||
|
/// Rename a phrase
|
||||||
Rename(Rename),
|
Rename(Rename),
|
||||||
|
/// Change the length of a phrase
|
||||||
Length(Length),
|
Length(Length),
|
||||||
|
/// Import from file
|
||||||
Import(Browse),
|
Import(Browse),
|
||||||
|
/// Export to file
|
||||||
Export(Browse),
|
Export(Browse),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -201,15 +217,6 @@ impl Command<PhraseListModel> for PhrasesCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasPhrases for PhraseListModel {
|
|
||||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
|
||||||
&self.phrases
|
|
||||||
}
|
|
||||||
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
|
|
||||||
&mut self.phrases
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InputToCommand<Tui, PhraseListModel> for PhrasesCommand {
|
impl InputToCommand<Tui, PhraseListModel> for PhrasesCommand {
|
||||||
fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option<Self> {
|
fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option<Self> {
|
||||||
Some(match state.phrases_mode() {
|
Some(match state.phrases_mode() {
|
||||||
|
|
@ -233,10 +240,10 @@ fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option<Phra
|
||||||
key!(Char('m')) => Cmd::Import(Browse::Begin),
|
key!(Char('m')) => Cmd::Import(Browse::Begin),
|
||||||
key!(Char('x')) => Cmd::Export(Browse::Begin),
|
key!(Char('x')) => Cmd::Export(Browse::Begin),
|
||||||
key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())),
|
key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())),
|
||||||
key!(Up) | key!(Char('[')) => Cmd::Select(
|
key!(Char('[')) | key!(Up) => Cmd::Select(
|
||||||
index.overflowing_sub(1).0.min(state.phrases().len() - 1)
|
index.overflowing_sub(1).0.min(state.phrases().len() - 1)
|
||||||
),
|
),
|
||||||
key!(Down) | key!(Char(']')) => Cmd::Select(
|
key!(Char(']')) | key!(Down) => Cmd::Select(
|
||||||
index.saturating_add(1) % state.phrases().len()
|
index.saturating_add(1) % state.phrases().len()
|
||||||
),
|
),
|
||||||
key!(Char('<')) => if index > 1 {
|
key!(Char('<')) => if index > 1 {
|
||||||
|
|
|
||||||
|
|
@ -2,55 +2,38 @@ use crate::*;
|
||||||
|
|
||||||
pub struct PhraseSelector {
|
pub struct PhraseSelector {
|
||||||
pub(crate) title: &'static str,
|
pub(crate) title: &'static str,
|
||||||
pub(crate) phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
pub(crate) name: String,
|
||||||
|
pub(crate) color: ItemPalette,
|
||||||
pub(crate) time: String,
|
pub(crate) time: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Display phrases always in order of appearance
|
// TODO: Display phrases always in order of appearance
|
||||||
render!(|self: PhraseSelector|{
|
render!(|self: PhraseSelector|Tui::fixed_y(1, lay!(move|add|{
|
||||||
let Self { title, phrase, time } = self;
|
add(&Tui::push_x(1, Tui::fg(TuiTheme::g(240), self.title)))?;
|
||||||
let border_bg = TuiTheme::border_bg();
|
add(&Tui::bg(self.color.base.rgb, Tui::fill_x(Tui::inset_x(1, Tui::fill_x(Tui::at_e(Tui::fg(self.color.lightest.rgb, &self.time)))))))?;
|
||||||
let border_color = TuiTheme::bo1();
|
|
||||||
let border = Lozenge(Style::default().bg(border_bg).fg(border_color));
|
|
||||||
let title_color = TuiTheme::g(200);
|
|
||||||
Tui::fixed_y(2, lay!(move|add|{
|
|
||||||
//if phrase.is_none() {
|
|
||||||
//add(&Tui::fill_x(border))?;
|
|
||||||
//}
|
|
||||||
add(&Tui::push_x(1, Tui::fg(title_color, *title)))?;
|
|
||||||
if let Some((_started, Some(phrase))) = phrase {
|
|
||||||
let Phrase { ref name, color, .. } = *phrase.read().unwrap();
|
|
||||||
add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([
|
|
||||||
Tui::fill_x(lay!([
|
|
||||||
Tui::fill_x(Tui::at_w(Tui::fg(TuiTheme::g(255), format!(" ")))),
|
|
||||||
Tui::inset_x(1, Tui::fill_x(Tui::at_e(Tui::fg(TuiTheme::g(255), time))))
|
|
||||||
])),
|
|
||||||
Tui::bold(true, Tui::fg(TuiTheme::g(255), format!(" {name}")))
|
|
||||||
]))))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}))
|
})));
|
||||||
});
|
|
||||||
impl PhraseSelector {
|
impl PhraseSelector {
|
||||||
// beats elapsed
|
// beats elapsed
|
||||||
pub fn play_phrase <T: HasPlayPhrase + HasClock> (state: &T) -> Self {
|
pub fn play_phrase <T: HasPlayPhrase + HasClock> (state: &T) -> Self {
|
||||||
let phrase = state.play_phrase().clone();
|
let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() {
|
||||||
Self {
|
let Phrase { ref name, color, .. } = *phrase.read().unwrap();
|
||||||
title: "Now:",
|
(name.clone(), color)
|
||||||
time: if let Some(elapsed) = state.pulses_since_start_looped() {
|
} else {
|
||||||
|
("".to_string(), ItemPalette::from(TuiTheme::g(64)))
|
||||||
|
};
|
||||||
|
let time = if let Some(elapsed) = state.pulses_since_start_looped() {
|
||||||
format!("+{:>}", state.clock().timebase.format_beats_0(elapsed))
|
format!("+{:>}", state.clock().timebase.format_beats_0(elapsed))
|
||||||
} else {
|
} else {
|
||||||
String::from("")
|
String::from("")
|
||||||
},
|
};
|
||||||
phrase,
|
Self { title: "Now:", time, name, color, }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// beats until switchover
|
// beats until switchover
|
||||||
pub fn next_phrase <T: HasPlayPhrase> (state: &T) -> Self {
|
pub fn next_phrase <T: HasPlayPhrase> (state: &T) -> Self {
|
||||||
let phrase = state.next_phrase().clone();
|
let (time, name, color) = if let Some((t, Some(phrase))) = state.next_phrase() {
|
||||||
Self {
|
let Phrase { ref name, color, .. } = *phrase.read().unwrap();
|
||||||
title: "Next:",
|
let time = {
|
||||||
time: phrase.as_ref().map(|(t, _)|{
|
|
||||||
let target = t.pulse.get();
|
let target = t.pulse.get();
|
||||||
let current = state.clock().playhead.pulse.get();
|
let current = state.clock().playhead.pulse.get();
|
||||||
if target > current {
|
if target > current {
|
||||||
|
|
@ -59,16 +42,20 @@ impl PhraseSelector {
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
}).unwrap_or(String::from("")),
|
};
|
||||||
phrase,
|
(time, name.clone(), color)
|
||||||
}
|
} else {
|
||||||
|
("".into(), "".into(), TuiTheme::g(64).into())
|
||||||
|
};
|
||||||
|
Self { title: "Next:", time, name, color, }
|
||||||
}
|
}
|
||||||
pub fn edit_phrase (phrase: &Option<Arc<RwLock<Phrase>>>) -> Self {
|
pub fn edit_phrase (phrase: &Option<Arc<RwLock<Phrase>>>) -> Self {
|
||||||
let phrase = phrase.clone();
|
let (time, name, color) = if let Some(phrase) = phrase {
|
||||||
Self {
|
let phrase = phrase.read().unwrap();
|
||||||
title: "Edit:",
|
(format!("{}", phrase.length), phrase.name.clone(), phrase.color)
|
||||||
time: phrase.as_ref().map(|p|format!("{}", p.read().unwrap().length)).unwrap_or(String::new()),
|
} else {
|
||||||
phrase: Some((Moment::default(), phrase)),
|
("".to_string(), "".to_string(), ItemPalette::from(TuiTheme::g(64)))
|
||||||
}
|
};
|
||||||
|
Self { title: "Editing:", time, name, color }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
207
crates/tek/src/tui/piano_horizontal.rs
Normal file
207
crates/tek/src/tui/piano_horizontal.rs
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
use crate::*;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub struct PianoHorizontal {
|
||||||
|
pub(crate) time_zoom: Option<usize>,
|
||||||
|
pub(crate) note_zoom: PhraseViewNoteZoom,
|
||||||
|
pub(crate) buffer: BigBuffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for PianoHorizontal {
|
||||||
|
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
|
f.debug_struct("PianoHorizontal")
|
||||||
|
.field("time_zoom", &self.time_zoom)
|
||||||
|
.field("note_zoom", &self.note_zoom)
|
||||||
|
.field("buffer", &format!("{}x{}", self.buffer.width, self.buffer.height))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub enum PhraseViewNoteZoom {
|
||||||
|
N(usize),
|
||||||
|
Half,
|
||||||
|
Octant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PhraseViewMode for PianoHorizontal {
|
||||||
|
fn time_zoom (&self) -> Option<usize> {
|
||||||
|
self.time_zoom
|
||||||
|
}
|
||||||
|
fn set_time_zoom (&mut self, time_zoom: Option<usize>) {
|
||||||
|
self.time_zoom = time_zoom
|
||||||
|
}
|
||||||
|
fn show (&mut self, phrase: Option<&Phrase>, note_len: usize) {
|
||||||
|
if let Some(phrase) = phrase {
|
||||||
|
self.buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase));
|
||||||
|
draw_piano_horizontal_bg(&mut self.buffer, phrase, self.time_zoom.unwrap(), note_len);
|
||||||
|
draw_piano_horizontal_fg(&mut self.buffer, phrase, self.time_zoom.unwrap());
|
||||||
|
} else {
|
||||||
|
self.buffer = Default::default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn buffer_width (&self, phrase: &Phrase) -> usize {
|
||||||
|
phrase.length / self.time_zoom.unwrap()
|
||||||
|
}
|
||||||
|
/// Determine the required height to render the phrase.
|
||||||
|
fn buffer_height (&self, phrase: &Phrase) -> usize {
|
||||||
|
match self.note_zoom {
|
||||||
|
PhraseViewNoteZoom::Half => 64,
|
||||||
|
PhraseViewNoteZoom::N(n) => 128*n,
|
||||||
|
_ => unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn render_notes (
|
||||||
|
&self,
|
||||||
|
target: &mut TuiOutput,
|
||||||
|
time_start: usize,
|
||||||
|
note_hi: usize,
|
||||||
|
) {
|
||||||
|
let [x0, y0, w, h] = target.area().xywh();
|
||||||
|
let source = &self.buffer;
|
||||||
|
let target = &mut target.buffer;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn render_keys (
|
||||||
|
&self,
|
||||||
|
to: &mut TuiOutput,
|
||||||
|
color: Color,
|
||||||
|
point: Option<usize>,
|
||||||
|
(note_lo, note_hi): (usize, usize)
|
||||||
|
) {
|
||||||
|
let [x, y0, _, _] = to.area().xywh();
|
||||||
|
let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0)));
|
||||||
|
let note_off_style = Some(Style::default().fg(TuiTheme::g(160)));
|
||||||
|
let note_on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(color).bold());
|
||||||
|
for (y, note) in (note_lo..=note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) {
|
||||||
|
let key = match note % 12 {
|
||||||
|
11 => "████▌",
|
||||||
|
10 => " ",
|
||||||
|
9 => "████▌",
|
||||||
|
8 => " ",
|
||||||
|
7 => "████▌",
|
||||||
|
6 => " ",
|
||||||
|
5 => "████▌",
|
||||||
|
4 => "████▌",
|
||||||
|
3 => " ",
|
||||||
|
2 => "████▌",
|
||||||
|
1 => " ",
|
||||||
|
0 => "████▌",
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
to.blit(&key, x, y, key_style);
|
||||||
|
|
||||||
|
if Some(note) == point {
|
||||||
|
to.blit(&format!("{:<5}", to_note_name(note)), x, y, note_on_style)
|
||||||
|
} else {
|
||||||
|
to.blit(&to_note_name(note), x, y, note_off_style)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn render_cursor (
|
||||||
|
&self,
|
||||||
|
to: &mut TuiOutput,
|
||||||
|
time_point: usize,
|
||||||
|
time_start: usize,
|
||||||
|
note_point: usize,
|
||||||
|
note_len: usize,
|
||||||
|
note_hi: usize,
|
||||||
|
note_lo: usize,
|
||||||
|
) {
|
||||||
|
let time_zoom = self.time_zoom.unwrap();
|
||||||
|
let [x0, y0, w, h] = to.area().xywh();
|
||||||
|
let style = Some(Style::default().fg(Color::Rgb(0,255,0)));
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
||||||
|
fn draw_piano_horizontal_bg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize, note_len: 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();
|
||||||
|
cell.set_bg(phrase.color.darkest.rgb);
|
||||||
|
cell.set_fg(phrase.color.darker.rgb);
|
||||||
|
cell.set_char(if time % 384 == 0 {
|
||||||
|
'│'
|
||||||
|
} else if time % 96 == 0 {
|
||||||
|
'╎'
|
||||||
|
} else if time % note_len == 0 {
|
||||||
|
'┊'
|
||||||
|
} else if (127 - note) % 12 == 1 {
|
||||||
|
'='
|
||||||
|
} else {
|
||||||
|
'·'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the piano roll background using full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
||||||
|
fn draw_piano_horizontal_fg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize) {
|
||||||
|
let style = Style::default().fg(phrase.color.lightest.rgb);//.bg(Color::Rgb(0, 0, 0));
|
||||||
|
let mut notes_on = [false;128];
|
||||||
|
for (x, time_start) in (0..phrase.length).step_by(zoom).enumerate() {
|
||||||
|
|
||||||
|
for (y, note) in (0..127).rev().enumerate() {
|
||||||
|
let cell = buf.get_mut(x, note).unwrap();
|
||||||
|
if notes_on[note] {
|
||||||
|
cell.set_char('▂');
|
||||||
|
cell.set_style(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let time_end = time_start + zoom;
|
||||||
|
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 = buf.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
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -116,7 +116,7 @@ impl From<&SequencerTui> for SequencerStatusBar {
|
||||||
}
|
}
|
||||||
|
|
||||||
render!(|self: SequencerStatusBar|{
|
render!(|self: SequencerStatusBar|{
|
||||||
lay!(|add|if self.width > 60 {
|
lay!(|add|if self.width > 40 {
|
||||||
add(&Tui::fill_x(Tui::fixed_y(1, lay!([
|
add(&Tui::fill_x(Tui::fixed_y(1, lay!([
|
||||||
Tui::fill_x(Tui::at_w(SequencerMode::from(self))),
|
Tui::fill_x(Tui::at_w(SequencerMode::from(self))),
|
||||||
Tui::fill_x(Tui::at_e(SequencerStats::from(self))),
|
Tui::fill_x(Tui::at_e(SequencerStats::from(self))),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue