mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
wip: phrase view mode refactor
This commit is contained in:
parent
1661814164
commit
1b44dc0ce8
7 changed files with 552 additions and 496 deletions
|
|
@ -23,7 +23,6 @@ mod phrase_length; pub(crate) use phrase_length::*;
|
|||
mod phrase_rename; pub(crate) use phrase_rename::*;
|
||||
mod phrase_list; pub(crate) use phrase_list::*;
|
||||
mod phrase_player; pub(crate) use phrase_player::*;
|
||||
mod phrase_select; pub(crate) use phrase_select::*;
|
||||
mod port_select; pub(crate) use port_select::*;
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -21,10 +21,6 @@ impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerTui {
|
|||
phrases.phrases.push(phrase.clone());
|
||||
phrases.phrase.store(1, Ordering::Relaxed);
|
||||
|
||||
let mut editor = PhraseEditorModel::default();
|
||||
editor.show_phrase(Some(phrase.clone()));
|
||||
editor.edit_mode = PhraseEditMode::Note;
|
||||
|
||||
let mut player = PhrasePlayerModel::from(&clock);
|
||||
player.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase)));
|
||||
|
||||
|
|
@ -32,7 +28,7 @@ impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerTui {
|
|||
clock,
|
||||
phrases,
|
||||
player,
|
||||
editor,
|
||||
editor: PhraseEditorModel::from(&phrase),
|
||||
jack: jack.clone(),
|
||||
size: Measure::new(),
|
||||
cursor: (0, 0),
|
||||
|
|
@ -79,20 +75,20 @@ impl Audio for SequencerTui {
|
|||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
// Start profiling cycle
|
||||
let t0 = self.perf.get_t0();
|
||||
|
||||
// Update transport clock
|
||||
if ClockAudio(self).process(client, scope) == Control::Quit {
|
||||
if ClockAudio(self)
|
||||
.process(client, scope) == Control::Quit
|
||||
{
|
||||
return Control::Quit
|
||||
}
|
||||
|
||||
// Update MIDI sequencer
|
||||
if PlayerAudio(
|
||||
&mut self.player,
|
||||
&mut self.note_buf,
|
||||
&mut self.midi_buf,
|
||||
).process(client, scope) == Control::Quit {
|
||||
if PlayerAudio(&mut self.player, &mut self.note_buf, &mut self.midi_buf)
|
||||
.process(client, scope) == Control::Quit
|
||||
{
|
||||
return Control::Quit
|
||||
}
|
||||
// End profiling cycle
|
||||
self.perf.update(t0, scope);
|
||||
|
||||
// Update sequencer playhead indicator
|
||||
//self.now().set(0.);
|
||||
|
|
@ -105,37 +101,105 @@ impl Audio for SequencerTui {
|
|||
//self.now().set(now);
|
||||
//}
|
||||
//}
|
||||
// End profiling cycle
|
||||
self.perf.update(t0, scope);
|
||||
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1,
|
||||
Tui::fill_xy(SequencerStatusBar::from(self)),
|
||||
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!([
|
||||
PhraseSelector::play_phrase(&self.player),
|
||||
PhraseSelector::next_phrase(&self.player),
|
||||
]), Tui::split_up(false, 2,
|
||||
PhraseSelector::edit_phrase(&self.editor.phrase),
|
||||
PhraseListView::from(self),
|
||||
))),
|
||||
col!([
|
||||
Tui::fixed_y(2, TransportView::from((
|
||||
self,
|
||||
self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten(),
|
||||
if let SequencerFocus::Transport(_) = self.focus {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
render!(|self: SequencerTui|{
|
||||
|
||||
let content = lay!([self.size, Tui::split_up(false, 1,
|
||||
Tui::fill_xy(SequencerStatusBar::from(self)),
|
||||
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!([
|
||||
PhraseSelector::play_phrase(&self.player),
|
||||
PhraseSelector::next_phrase(&self.player),
|
||||
]), Tui::split_up(false, 2,
|
||||
PhraseSelector::edit_phrase(&self.editor.phrase.read().unwrap()),
|
||||
PhraseListView::from(self),
|
||||
))),
|
||||
PhraseView::from(self)
|
||||
]),
|
||||
)
|
||||
)]));
|
||||
col!([
|
||||
Tui::fixed_y(2, TransportView::from((
|
||||
self,
|
||||
self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten(),
|
||||
if let SequencerFocus::Transport(_) = self.focus {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
))),
|
||||
PhraseView::from(self)
|
||||
]),
|
||||
)
|
||||
)]);
|
||||
|
||||
pub struct PhraseSelector {
|
||||
pub(crate) title: &'static str,
|
||||
pub(crate) name: String,
|
||||
pub(crate) color: ItemPalette,
|
||||
pub(crate) time: String,
|
||||
}
|
||||
|
||||
// TODO: Display phrases always in order of appearance
|
||||
render!(|self: PhraseSelector|Tui::fixed_y(2, col!([
|
||||
lay!(move|add|{
|
||||
add(&Tui::push_x(1, Tui::fg(TuiTheme::g(240), self.title)))?;
|
||||
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)))))))?;
|
||||
Ok(())
|
||||
}),
|
||||
Tui::bg(self.color.base.rgb,
|
||||
Tui::fg(self.color.lightest.rgb,
|
||||
Tui::bold(true, self.name.clone()))),
|
||||
])));
|
||||
|
||||
impl PhraseSelector {
|
||||
// beats elapsed
|
||||
pub fn play_phrase <T: HasPlayPhrase + HasClock> (state: &T) -> Self {
|
||||
let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() {
|
||||
let Phrase { ref name, color, .. } = *phrase.read().unwrap();
|
||||
(name.clone(), color)
|
||||
} 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))
|
||||
} else {
|
||||
String::from("")
|
||||
};
|
||||
Self { title: "Now:", time, name, color, }
|
||||
}
|
||||
// beats until switchover
|
||||
pub fn next_phrase <T: HasPlayPhrase> (state: &T) -> Self {
|
||||
let (time, name, color) = if let Some((t, Some(phrase))) = state.next_phrase() {
|
||||
let Phrase { ref name, color, .. } = *phrase.read().unwrap();
|
||||
let time = {
|
||||
let target = t.pulse.get();
|
||||
let current = state.clock().playhead.pulse.get();
|
||||
if target > current {
|
||||
let remaining = target - current;
|
||||
format!("-{:>}", state.clock().timebase.format_beats_0(remaining))
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
(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 {
|
||||
let (time, name, color) = if let Some(phrase) = phrase {
|
||||
let phrase = phrase.read().unwrap();
|
||||
(format!("{}", phrase.length), phrase.name.clone(), phrase.color)
|
||||
} else {
|
||||
("".to_string(), "".to_string(), ItemPalette::from(TuiTheme::g(64)))
|
||||
};
|
||||
Self { title: "Editing:", time, name, color }
|
||||
}
|
||||
}
|
||||
content
|
||||
});
|
||||
|
||||
impl HasClock for SequencerTui {
|
||||
fn clock (&self) -> &ClockModel {
|
||||
|
|
@ -266,20 +330,13 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option<S
|
|||
_ => SequencerCommand::Focus(FocusCommand::Set(PhraseEditor)),
|
||||
}
|
||||
|
||||
// Esc: toggle between scrolling and editing
|
||||
key!(Esc) =>
|
||||
Editor(SetEditMode(match state.editor.edit_mode {
|
||||
PhraseEditMode::Scroll => PhraseEditMode::Note,
|
||||
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
|
||||
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.read().unwrap().map(|p|p.read().unwrap().clone());
|
||||
let selected_phrase = state.phrases.phrase().clone();
|
||||
if Some(selected_phrase.read().unwrap().clone()) != editing_phrase {
|
||||
Editor(Show(Some(selected_phrase)))
|
||||
|
|
|
|||
|
|
@ -1,19 +1,6 @@
|
|||
use crate::*;
|
||||
use ratatui::buffer::Cell;
|
||||
|
||||
/// Every struct that has [Content]<[Tui]> is a renderable [Render]<[Tui]>.
|
||||
//impl<C: Content<Tui>> Render<Tui> for C {
|
||||
//fn min_size (&self, to: [u16;2]) -> Perhaps<E::Size> {
|
||||
//self.content().min_size(to)
|
||||
//}
|
||||
//fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
//match self.min_size(to.area().wh().into())? {
|
||||
//Some(wh) => to.render_in(to.area().clip(wh).into(), &self.content()),
|
||||
//None => Ok(())
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
|
||||
pub struct TuiOutput {
|
||||
pub buffer: Buffer,
|
||||
pub area: [u16;4]
|
||||
|
|
@ -86,22 +73,6 @@ impl TuiOutput {
|
|||
}
|
||||
}
|
||||
|
||||
//impl Area<u16> for Rect {
|
||||
//fn x (&self) -> u16 { self.x }
|
||||
//fn y (&self) -> u16 { self.y }
|
||||
//fn w (&self) -> u16 { self.width }
|
||||
//fn h (&self) -> u16 { self.height }
|
||||
//}
|
||||
|
||||
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
|
||||
match (lower, upper) {
|
||||
(true, true) => Some('█'),
|
||||
(true, false) => Some('▄'),
|
||||
(false, true) => Some('▀'),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BigBuffer {
|
||||
pub width: usize,
|
||||
|
|
@ -126,6 +97,12 @@ impl BigBuffer {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<(usize, usize)> for BigBuffer { // cuteness overload
|
||||
fn from ((width, height): (usize, usize)) -> Self {
|
||||
Self::new(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
|
||||
for row in 0..area.h() {
|
||||
let y = area.y() + row;
|
||||
|
|
@ -138,6 +115,22 @@ pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut C
|
|||
}
|
||||
}
|
||||
|
||||
//impl Area<u16> for Rect {
|
||||
//fn x (&self) -> u16 { self.x }
|
||||
//fn y (&self) -> u16 { self.y }
|
||||
//fn w (&self) -> u16 { self.width }
|
||||
//fn h (&self) -> u16 { self.height }
|
||||
//}
|
||||
|
||||
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
|
||||
match (lower, upper) {
|
||||
(true, true) => Some('█'),
|
||||
(true, false) => Some('▄'),
|
||||
(false, true) => Some('▀'),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
impl Render<Tui> for () {
|
||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
Ok(None)
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@ pub trait HasEditor {
|
|||
/// Contains state for viewing and editing a phrase
|
||||
pub struct PhraseEditorModel {
|
||||
/// Phrase being played
|
||||
pub(crate) phrase: Option<Arc<RwLock<Phrase>>>,
|
||||
pub(crate) phrase: Arc<RwLock<Option<Arc<RwLock<Phrase>>>>>,
|
||||
/// Renders the 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
|
||||
pub(crate) note_len: usize,
|
||||
pub(crate) note_len: Arc<AtomicUsize>,
|
||||
/// Notes currently held at input
|
||||
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
|
||||
/// Notes currently held at output
|
||||
|
|
@ -32,32 +32,18 @@ pub struct PhraseEditorModel {
|
|||
pub(crate) size: Measure<Tui>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum PhraseEditMode {
|
||||
Note,
|
||||
Scroll,
|
||||
impl From<&Arc<RwLock<Phrase>>> for PhraseEditorModel {
|
||||
fn from (phrase: &Arc<RwLock<Phrase>>) -> Self {
|
||||
Self::from(Some(phrase.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PhraseViewMode: Debug + Send + Sync {
|
||||
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,
|
||||
);
|
||||
impl From<Option<Arc<RwLock<Phrase>>>> for PhraseEditorModel {
|
||||
fn from (phrase: Option<Arc<RwLock<Phrase>>>) -> Self {
|
||||
let model = Self::default();
|
||||
*model.phrase.write().unwrap() = phrase;
|
||||
model
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PhraseEditorModel {
|
||||
|
|
@ -77,21 +63,23 @@ impl std::fmt::Debug for PhraseEditorModel {
|
|||
|
||||
impl Default for PhraseEditorModel {
|
||||
fn default () -> Self {
|
||||
let phrase = Arc::new(RwLock::new(None));
|
||||
Self {
|
||||
phrase: None,
|
||||
note_len: 24,
|
||||
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(),
|
||||
phrase: phrase.clone(),
|
||||
now: Pulse::default().into(),
|
||||
time_start: 0.into(),
|
||||
time_point: 0.into(),
|
||||
note_lo: 0.into(),
|
||||
note_point: 0.into(),
|
||||
note_len: Arc::from(AtomicUsize::from(24)),
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
view_mode: Box::new(PianoHorizontal {
|
||||
phrase,
|
||||
buffer: Default::default(),
|
||||
time_zoom: Some(24),
|
||||
time_zoom: 24,
|
||||
time_lock: true,
|
||||
note_zoom: PhraseViewNoteZoom::N(1)
|
||||
}),
|
||||
}
|
||||
|
|
@ -101,17 +89,18 @@ impl Default for PhraseEditorModel {
|
|||
impl PhraseEditorModel {
|
||||
/// Put note at current position
|
||||
pub fn put_note (&mut self) {
|
||||
if let Some(phrase) = &self.phrase {
|
||||
if let Some(phrase) = *self.phrase.read().unwrap() {
|
||||
let note_len = self.note_len.load(Ordering::Relaxed);
|
||||
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;
|
||||
let end = (start + note_len) % phrase.length;
|
||||
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
|
||||
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
|
||||
self.view_mode.show(Some(&phrase), self.note_len);
|
||||
self.view_mode.show(Some(&phrase), note_len);
|
||||
}
|
||||
}
|
||||
/// Move time cursor forward by current note length
|
||||
|
|
@ -134,119 +123,14 @@ 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 trait PhraseViewMode: Debug + Send + Sync {
|
||||
fn time_zoom (&self) -> usize;
|
||||
fn set_time_zoom (&mut self, time_zoom: usize);
|
||||
fn time_zoom_lock (&self) -> bool;
|
||||
fn set_time_zoom_lock (&mut self, time_zoom: bool);
|
||||
fn buffer_size (&self, phrase: &Phrase) -> (usize, usize);
|
||||
fn redraw (&mut self);
|
||||
fn phrase (&self) -> &Arc<RwLock<Option<Arc<RwLock<Phrase>>>>>;
|
||||
}
|
||||
|
||||
pub struct PhraseView<'a> {
|
||||
|
|
@ -255,7 +139,7 @@ pub struct PhraseView<'a> {
|
|||
time_start: usize,
|
||||
time_point: usize,
|
||||
note_len: usize,
|
||||
phrase: &'a Option<Arc<RwLock<Phrase>>>,
|
||||
phrase: Arc<RwLock<Option<Arc<RwLock<Phrase>>>>>,
|
||||
view_mode: &'a Box<dyn PhraseViewMode>,
|
||||
now: &'a Arc<Pulse>,
|
||||
size: &'a Measure<Tui>,
|
||||
|
|
@ -269,7 +153,7 @@ render!(|self: PhraseView<'a>|{
|
|||
.map(|p|p.read().unwrap().color.clone())
|
||||
.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64))));
|
||||
Tui::bg(bg, Tui::split_up(false, 2,
|
||||
Tui::bg(fg.dark.rgb, lay!([
|
||||
Tui::bg(fg.dark.rgb, col!([
|
||||
PhraseTimeline(&self, fg),
|
||||
PhraseViewStats(&self, fg),
|
||||
])),
|
||||
|
|
@ -297,8 +181,8 @@ impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> {
|
|||
note_range: (note_lo, note_hi),
|
||||
time_start: editor.time_start.load(Ordering::Relaxed),
|
||||
time_point: editor.time_point.load(Ordering::Relaxed),
|
||||
note_len: editor.note_len,
|
||||
phrase: &editor.phrase,
|
||||
note_len: editor.note_len.load(Ordering::Relaxed),
|
||||
phrase: editor.phrase.clone(),
|
||||
view_mode: &editor.view_mode,
|
||||
size: &editor.size,
|
||||
now: &editor.now,
|
||||
|
|
@ -363,3 +247,112 @@ render!(|self: PhraseCursor<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|Ok(
|
|||
self.0.note_range.0,
|
||||
)
|
||||
))));
|
||||
|
||||
#[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),
|
||||
SetTimeZoomLock(bool),
|
||||
Show(Option<Arc<RwLock<Phrase>>>),
|
||||
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.read().unwrap().map(|p|p.read().unwrap().length).unwrap_or(1);
|
||||
let note_len = state.note_len.load(Ordering::Relaxed);
|
||||
Some(match from.event() {
|
||||
key!(Char('`')) => ToggleDirection,
|
||||
key!(Char('z')) => SetTimeZoomLock(!state.view_mode.time_zoom_lock()),
|
||||
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,
|
||||
// TODO: no triplet/dotted
|
||||
key!(Char(',')) => SetNoteLength(prev_note_length(note_len)),
|
||||
key!(Char('.')) => SetNoteLength(next_note_length(note_len)),
|
||||
// TODO: with triplet/dotted
|
||||
key!(Char('<')) => SetNoteLength(prev_note_length(note_len)),
|
||||
key!(Char('>')) => SetNoteLength(next_note_length(note_len)),
|
||||
// TODO: '/' set triplet, '?' set dotted
|
||||
_ => match from.event() {
|
||||
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)),
|
||||
key!(Shift-Right) => SetTimeCursor((time_point + time_zoom) % length),
|
||||
|
||||
key!(Ctrl-Up) => SetNoteScroll(note_lo + 1),
|
||||
key!(Ctrl-Down) => SetNoteScroll(note_lo.saturating_sub(1)),
|
||||
key!(Ctrl-PageUp) => SetNoteScroll(note_point + 3),
|
||||
key!(Ctrl-PageDown) => SetNoteScroll(note_point.saturating_sub(3)),
|
||||
key!(Ctrl-Left) => SetTimeScroll(time_start.saturating_sub(note_len)),
|
||||
key!(Ctrl-Right) => SetTimeScroll(time_start + note_len),
|
||||
|
||||
|
||||
|
||||
_ => 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);
|
||||
},
|
||||
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());
|
||||
},
|
||||
SetTimeZoomLock(lock) => {
|
||||
state.view_mode.set_zoom_lock(lock);
|
||||
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.store(time, Ordering::Relaxed); },
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct PhraseSelector {
|
||||
pub(crate) title: &'static str,
|
||||
pub(crate) name: String,
|
||||
pub(crate) color: ItemPalette,
|
||||
pub(crate) time: String,
|
||||
}
|
||||
|
||||
// TODO: Display phrases always in order of appearance
|
||||
render!(|self: PhraseSelector|Tui::fixed_y(1, lay!(move|add|{
|
||||
add(&Tui::push_x(1, Tui::fg(TuiTheme::g(240), self.title)))?;
|
||||
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)))))))?;
|
||||
Ok(())
|
||||
})));
|
||||
impl PhraseSelector {
|
||||
// beats elapsed
|
||||
pub fn play_phrase <T: HasPlayPhrase + HasClock> (state: &T) -> Self {
|
||||
let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() {
|
||||
let Phrase { ref name, color, .. } = *phrase.read().unwrap();
|
||||
(name.clone(), color)
|
||||
} 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))
|
||||
} else {
|
||||
String::from("")
|
||||
};
|
||||
Self { title: "Now:", time, name, color, }
|
||||
}
|
||||
// beats until switchover
|
||||
pub fn next_phrase <T: HasPlayPhrase> (state: &T) -> Self {
|
||||
let (time, name, color) = if let Some((t, Some(phrase))) = state.next_phrase() {
|
||||
let Phrase { ref name, color, .. } = *phrase.read().unwrap();
|
||||
let time = {
|
||||
let target = t.pulse.get();
|
||||
let current = state.clock().playhead.pulse.get();
|
||||
if target > current {
|
||||
let remaining = target - current;
|
||||
format!("-{:>}", state.clock().timebase.format_beats_0(remaining))
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
(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 {
|
||||
let (time, name, color) = if let Some(phrase) = phrase {
|
||||
let phrase = phrase.read().unwrap();
|
||||
(format!("{}", phrase.length), phrase.name.clone(), phrase.color)
|
||||
} else {
|
||||
("".to_string(), "".to_string(), ItemPalette::from(TuiTheme::g(64)))
|
||||
};
|
||||
Self { title: "Editing:", time, name, color }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,251 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
|
||||
pub struct PianoHorizontal {
|
||||
pub(crate) time_zoom: Option<usize>,
|
||||
pub(crate) phrase: Arc<RwLock<Option<Arc<RwLock<Phrase>>>>>,
|
||||
pub(crate) time_lock: bool,
|
||||
pub(crate) time_zoom: usize,
|
||||
pub(crate) note_zoom: PhraseViewNoteZoom,
|
||||
pub(crate) note_len: Arc<AtomicUsize>,
|
||||
pub(crate) buffer: BigBuffer,
|
||||
pub(crate) focused: bool,
|
||||
}
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum PhraseViewNoteZoom {
|
||||
N(usize),
|
||||
Half,
|
||||
Octant,
|
||||
}
|
||||
render!(|self: PianoHorizontal|{
|
||||
let bg = if self.focused { TuiTheme::g(32) } else { Color::Reset };
|
||||
let fg = self.phrase().read().unwrap()
|
||||
.as_ref().map(|p|p.read().unwrap().color)
|
||||
.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64))));
|
||||
Tui::bg(bg, Tui::split_up(false, 2,
|
||||
Tui::bg(fg.dark.rgb, PianoHorizontalTimeline {
|
||||
start: "TIMELINE".into()
|
||||
}),
|
||||
Split::right(false, 5, PianoHorizontalKeys {
|
||||
color: ItemPalette::random(),
|
||||
note_lo: 0,
|
||||
note_hi: 0,
|
||||
note_point: None
|
||||
}, lay!([
|
||||
PianoHorizontalNotes {
|
||||
source: &self.buffer,
|
||||
time_start: 0,
|
||||
note_hi: 0,
|
||||
},
|
||||
PianoHorizontalCursor {
|
||||
time_zoom: 0,
|
||||
time_point: 0,
|
||||
time_start: 0,
|
||||
note_point: 0,
|
||||
note_len: 0,
|
||||
note_hi: 0,
|
||||
note_lo: 0,
|
||||
},
|
||||
])),
|
||||
))
|
||||
});
|
||||
pub struct PianoHorizontalTimeline {
|
||||
start: String
|
||||
}
|
||||
render!(|self: PianoHorizontalTimeline|{
|
||||
Tui::fg(TuiTheme::g(224), Tui::push_x(5, self.start.as_str()))
|
||||
});
|
||||
pub struct PianoHorizontalKeys {
|
||||
color: ItemPalette,
|
||||
note_lo: usize,
|
||||
note_hi: usize,
|
||||
note_point: Option<usize>,
|
||||
}
|
||||
render!(|self: PianoHorizontalKeys|render(|to|Ok({
|
||||
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 off_style = Some(Style::default().fg(TuiTheme::g(160)));
|
||||
let on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(self.color.base.rgb).bold());
|
||||
for (y, note) in (self.note_lo..=self.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) == self.note_point {
|
||||
to.blit(&format!("{:<5}", to_note_name(note)), x, y, on_style)
|
||||
} else {
|
||||
to.blit(&to_note_name(note), x, y, off_style)
|
||||
};
|
||||
}
|
||||
})));
|
||||
pub struct PianoHorizontalCursor {
|
||||
time_zoom: usize,
|
||||
time_point: usize,
|
||||
time_start: usize,
|
||||
note_point: usize,
|
||||
note_len: usize,
|
||||
note_hi: usize,
|
||||
note_lo: usize,
|
||||
}
|
||||
render!(|self: PianoHorizontalCursor|render(|to|Ok({
|
||||
let [x0, y0, w, _] = to.area().xywh();
|
||||
let style = Some(Style::default().fg(Color::Rgb(0,255,0)));
|
||||
for (y, note) in (self.note_lo..=self.note_hi).rev().enumerate() {
|
||||
if note == self.note_point {
|
||||
for x in 0..w {
|
||||
let time_1 = self.time_start + x as usize * self.time_zoom;
|
||||
let time_2 = time_1 + self.time_zoom;
|
||||
if time_1 <= self.time_point && self.time_point < time_2 {
|
||||
to.blit(&"█", x0 + x as u16, y0 + y as u16, style);
|
||||
let tail = self.note_len as u16 / self.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
|
||||
}
|
||||
}
|
||||
})));
|
||||
pub struct PianoHorizontalNotes<'a> {
|
||||
source: &'a BigBuffer,
|
||||
time_start: usize,
|
||||
note_hi: usize,
|
||||
}
|
||||
render!(|self: PianoHorizontalNotes<'a>|render(|to|Ok({
|
||||
let [x0, y0, w, h] = to.area().xywh();
|
||||
let target = &mut to.buffer;
|
||||
for (x, target_x) in (x0..x0+w).enumerate() {
|
||||
for (y, target_y) in (y0..y0+h).enumerate() {
|
||||
if y > self.note_hi {
|
||||
break
|
||||
}
|
||||
let source_x = self.time_start + x;
|
||||
let source_y = self.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 < self.source.width && source_y < self.source.height {
|
||||
let target_cell = target.get_mut(target_x, target_y);
|
||||
if let Some(source_cell) = self.source.get(source_x, source_y) {
|
||||
*target_cell = source_cell.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})));
|
||||
impl PianoHorizontal {
|
||||
/// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
||||
fn draw_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_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
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
impl PhraseViewMode for PianoHorizontal {
|
||||
fn phrase (&self) -> &Arc<RwLock<Option<Arc<RwLock<Phrase>>>>> {
|
||||
&self.phrase
|
||||
}
|
||||
fn time_zoom (&self) -> usize {
|
||||
self.time_zoom
|
||||
}
|
||||
fn set_time_zoom (&mut self, time_zoom: usize) {
|
||||
self.time_zoom = time_zoom;
|
||||
self.redraw()
|
||||
}
|
||||
fn time_zoom_lock (&self) -> bool {
|
||||
self.time_lock
|
||||
}
|
||||
fn set_time_zoom_lock (&mut self, time_lock: bool) {
|
||||
self.time_lock = time_lock;
|
||||
self.redraw()
|
||||
}
|
||||
/// Determine the required space to render the phrase.
|
||||
fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) {
|
||||
let width = phrase.length / self.time_zoom();
|
||||
let height = match self.note_zoom {
|
||||
PhraseViewNoteZoom::Half => 64,
|
||||
PhraseViewNoteZoom::N(n) => 128*n,
|
||||
_ => unimplemented!()
|
||||
};
|
||||
(width, height)
|
||||
}
|
||||
fn redraw (&mut self) {
|
||||
let buffer = if let Some(phrase) = &*self.phrase().read().unwrap() {
|
||||
let phrase = phrase.read().unwrap();
|
||||
let mut buffer = BigBuffer::from(self.buffer_size(&phrase));
|
||||
let note_len = self.note_len.load(Ordering::Relaxed);
|
||||
PianoHorizontal::draw_bg(&mut buffer, &phrase, self.time_zoom(), note_len);
|
||||
PianoHorizontal::draw_fg(&mut buffer, &phrase, self.time_zoom());
|
||||
buffer
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
self.buffer = buffer
|
||||
}
|
||||
}
|
||||
impl std::fmt::Debug for PianoHorizontal {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("PianoHorizontal")
|
||||
|
|
@ -17,191 +256,34 @@ impl std::fmt::Debug for PianoHorizontal {
|
|||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
//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;
|
||||
//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
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -71,10 +71,7 @@ impl From<&SequencerTui> for SequencerStatusBar {
|
|||
//PhrasePlay => " TO PLAY ",
|
||||
//PhraseNext => " UP NEXT ",
|
||||
PhraseList => " PHRASES ",
|
||||
PhraseEditor => match state.editor.edit_mode {
|
||||
PhraseEditMode::Note => " EDIT MIDI ",
|
||||
PhraseEditMode::Scroll => " VIEW MIDI ",
|
||||
},
|
||||
PhraseEditor => " EDIT MIDI ",
|
||||
},
|
||||
help: match state.focused() {
|
||||
Transport(PlayPause) => &[
|
||||
|
|
@ -101,14 +98,10 @@ impl From<&SequencerTui> for SequencerStatusBar {
|
|||
("", "⏎", " play"),
|
||||
("", "e", " edit"),
|
||||
],
|
||||
PhraseEditor => match state.editor.edit_mode {
|
||||
PhraseEditMode::Note => &[
|
||||
("", "✣", " cursor"),
|
||||
],
|
||||
PhraseEditMode::Scroll => &[
|
||||
("", "✣", " scroll"),
|
||||
],
|
||||
},
|
||||
PhraseEditor => &[
|
||||
("", "✣", " cursor"),
|
||||
("", "Ctrl-✣", " scroll"),
|
||||
],
|
||||
_ => default_help,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue