mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 12:16:42 +01:00
wip: fix piano roll area
This commit is contained in:
parent
0d1d7a05b9
commit
1c93646fcf
5 changed files with 144 additions and 120 deletions
|
|
@ -57,7 +57,7 @@ pub trait MidiRange {
|
||||||
fn set_note_lo (&self, x: usize);
|
fn set_note_lo (&self, x: usize);
|
||||||
fn note_axis (&self) -> usize;
|
fn note_axis (&self) -> usize;
|
||||||
fn time_axis (&self) -> usize;
|
fn time_axis (&self) -> usize;
|
||||||
fn note_hi (&self) -> usize { self.note_lo() + self.note_axis().saturating_sub(1) }
|
fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() }
|
||||||
fn time_end (&self) -> usize { self.time_start() + self.time_axis() * self.time_zoom() }
|
fn time_end (&self) -> usize { self.time_start() + self.time_axis() * self.time_zoom() }
|
||||||
}
|
}
|
||||||
impl MidiRange for MidiRangeModel {
|
impl MidiRange for MidiRangeModel {
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ impl<E: Engine, X: Render<E>, Y: Render<E>> Render<E> for Bsp<E, X, Y> {
|
||||||
let s_b = b.min_size(s)?.unwrap_or(n);
|
let s_b = b.min_size(s)?.unwrap_or(n);
|
||||||
let s_y = s_a.h().into();
|
let s_y = s_a.h().into();
|
||||||
to.render_in(to.area().clip_h(s_y).into(), a)?;
|
to.render_in(to.area().clip_h(s_y).into(), a)?;
|
||||||
to.render_in(to.area().push_y(s_y).into(), b)?;
|
to.render_in(to.area().push_y(s_y).shrink_y(s_y).into(), b)?;
|
||||||
},
|
},
|
||||||
Self::W(a, b) => {
|
Self::W(a, b) => {
|
||||||
let n = [0.into(), 0.into()].into();
|
let n = [0.into(), 0.into()].into();
|
||||||
|
|
|
||||||
|
|
@ -169,9 +169,9 @@ render!(|self: SequencerTui|{
|
||||||
let pool = Tui::fill_y(Tui::at_e(PhraseListView(&self.phrases)));
|
let pool = Tui::fill_y(Tui::at_e(PhraseListView(&self.phrases)));
|
||||||
let with_pool = move|x|Tui::split_w(false, pool_w, pool, x);
|
let with_pool = move|x|Tui::split_w(false, pool_w, pool, x);
|
||||||
let with_status = |x|Tui::split_n(false, 2, SequencerStatusBar::from(self), x);
|
let with_status = |x|Tui::split_n(false, 2, SequencerStatusBar::from(self), x);
|
||||||
let with_bar = |x|Tui::split_n(false, 3, PhraseEditStatus(&self.editor), x);
|
let with_editbar = |x|x;//Tui::split_n(false, 3, PhraseEditStatus(&self.editor), x);
|
||||||
let with_size = |x|lay!([self.size, x]);
|
let with_size = |x|lay!([self.size, x]);
|
||||||
let editor = with_bar(with_pool(Tui::fill_xy(&self.editor)));
|
let editor = with_editbar(with_pool(Tui::fill_xy(&self.editor)));
|
||||||
let color = self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten().clone();
|
let color = self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten().clone();
|
||||||
let play = Tui::fixed_xy(5, 2, PlayPause(self.clock.is_rolling()));
|
let play = Tui::fixed_xy(5, 2, PlayPause(self.clock.is_rolling()));
|
||||||
let transport = Tui::fixed_y(2, TransportView::from((self, color, true)));
|
let transport = Tui::fixed_y(2, TransportView::from((self, color, true)));
|
||||||
|
|
@ -187,76 +187,6 @@ has_clock!(|self:SequencerTui|&self.clock);
|
||||||
has_phrases!(|self:SequencerTui|self.phrases.phrases);
|
has_phrases!(|self:SequencerTui|self.phrases.phrases);
|
||||||
has_editor!(|self:SequencerTui|self.editor);
|
has_editor!(|self:SequencerTui|self.editor);
|
||||||
|
|
||||||
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_xy(24, 1, row!([
|
|
||||||
Tui::fg(self.color.lighter.rgb, Tui::bold(true, &self.title)),
|
|
||||||
Tui::fg_bg(self.color.lighter.rgb, self.color.base.rgb, row!([
|
|
||||||
format!("{:8}", &self.name[0..8.min(self.name.len())]),
|
|
||||||
Tui::bg(self.color.dark.rgb, &self.time),
|
|
||||||
])),
|
|
||||||
])));
|
|
||||||
|
|
||||||
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 if let Some((_, Some(phrase))) = state.play_phrase() {
|
|
||||||
let phrase = phrase.read().unwrap();
|
|
||||||
if phrase.loop_on {
|
|
||||||
(" ".into(), phrase.name.clone(), phrase.color.clone())
|
|
||||||
} else {
|
|
||||||
(" ".into(), " ".into(), TuiTheme::g(64).into())
|
|
||||||
}
|
|
||||||
} 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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HasPhraseList for SequencerTui {
|
impl HasPhraseList for SequencerTui {
|
||||||
fn phrases_focused (&self) -> bool {
|
fn phrases_focused (&self) -> bool {
|
||||||
true
|
true
|
||||||
|
|
|
||||||
|
|
@ -260,3 +260,73 @@ render!(|self: PhraseListView<'a>|{
|
||||||
add(&self.0.size)
|
add(&self.0.size)
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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_xy(24, 1, row!([
|
||||||
|
Tui::fg(self.color.lightest.rgb, Tui::bold(true, &self.title)),
|
||||||
|
Tui::fg_bg(self.color.lighter.rgb, self.color.base.rgb, row!([
|
||||||
|
format!("{:8}", &self.name[0..8.min(self.name.len())]),
|
||||||
|
Tui::bg(self.color.dark.rgb, &self.time),
|
||||||
|
])),
|
||||||
|
])));
|
||||||
|
|
||||||
|
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 if let Some((_, Some(phrase))) = state.play_phrase() {
|
||||||
|
let phrase = phrase.read().unwrap();
|
||||||
|
if phrase.loop_on {
|
||||||
|
(" ".into(), phrase.name.clone(), phrase.color.clone())
|
||||||
|
} else {
|
||||||
|
(" ".into(), " ".into(), TuiTheme::g(64).into())
|
||||||
|
}
|
||||||
|
} 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,11 @@ pub struct PianoHorizontalTimeline<'a> {
|
||||||
color: ItemPalette,
|
color: ItemPalette,
|
||||||
range: &'a MidiRangeModel,
|
range: &'a MidiRangeModel,
|
||||||
}
|
}
|
||||||
render!(|self: PianoHorizontalTimeline<'a>|Tui::fg_bg(
|
render!(|self: PianoHorizontalTimeline<'a>|Tui::fixed_y(1, Tui::fg_bg(
|
||||||
self.color.lightest.rgb,
|
self.color.lightest.rgb,
|
||||||
self.color.darkest.rgb,
|
self.color.darkest.rgb,
|
||||||
format!("{}*{}", self.range.time_start(), self.range.time_zoom()).as_str()
|
format!("{}*{}", self.range.time_start(), self.range.time_zoom()).as_str()
|
||||||
));
|
)));
|
||||||
|
|
||||||
pub struct PianoHorizontalKeys<'a> {
|
pub struct PianoHorizontalKeys<'a> {
|
||||||
color: ItemPalette,
|
color: ItemPalette,
|
||||||
|
|
@ -81,11 +81,10 @@ render!(|self: PianoHorizontalKeys<'a>|render(|to|Ok({
|
||||||
let note_lo = self.range.note_lo();
|
let note_lo = self.range.note_lo();
|
||||||
let note_hi = self.range.note_hi();
|
let note_hi = self.range.note_hi();
|
||||||
let note_point = self.point.note_point();
|
let note_point = self.point.note_point();
|
||||||
let [x, y0, ..] = to.area().xywh();
|
let [x, y0, w, h] = to.area().xywh();
|
||||||
let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0)));
|
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 off_style = Some(Style::default().fg(TuiTheme::g(160)));
|
||||||
let on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(self.color.light.rgb).bold());
|
let on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(self.color.light.rgb).bold());
|
||||||
|
|
||||||
for (y, note) in (note_lo..note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) {
|
for (y, note) in (note_lo..note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) {
|
||||||
let key = match note % 12 {
|
let key = match note % 12 {
|
||||||
11 => "████▌",
|
11 => "████▌",
|
||||||
|
|
@ -116,8 +115,10 @@ render!(|self: PianoHorizontalKeys<'a>|render(|to|Ok({
|
||||||
to.blit(&format!("XYU"), x, y0, None);
|
to.blit(&format!("XYU"), x, y0, None);
|
||||||
to.blit(&format!("x={x}"), x, y0+1, None);
|
to.blit(&format!("x={x}"), x, y0+1, None);
|
||||||
to.blit(&format!("y0={y0}"), x, y0+2, None);
|
to.blit(&format!("y0={y0}"), x, y0+2, None);
|
||||||
to.blit(&format!("note_lo={note_lo}"), x, y0+3, None);
|
to.blit(&format!("w={w}"), x, y0+3, None);
|
||||||
to.blit(&format!("note_hi={note_hi}"), x, y0+4, None);
|
to.blit(&format!("h={h}"), x, y0+4, None);
|
||||||
|
to.blit(&format!("note_lo={note_lo}"), x, y0+5, None);
|
||||||
|
to.blit(&format!("note_hi={note_hi}"), x, y0+6, None);
|
||||||
}
|
}
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
|
@ -135,16 +136,17 @@ render!(|self: PianoHorizontalCursor<'a>|render(|to|Ok({
|
||||||
let time_start = self.range.time_start();
|
let time_start = self.range.time_start();
|
||||||
let time_zoom = self.range.time_zoom();
|
let time_zoom = self.range.time_zoom();
|
||||||
let style = Some(Style::default().fg(Color::Rgb(0,255,0)));
|
let style = Some(Style::default().fg(Color::Rgb(0,255,0)));
|
||||||
for (y, note) in (note_lo..=note_hi).rev().enumerate() {
|
for (input_y, output_y, note) in (note_lo..note_hi).rev().enumerate().map(|(y, n)|(y, y0 + y as u16, n)) {
|
||||||
if note == note_point {
|
if note == note_point {
|
||||||
for x in 0..w {
|
for x in 0..w {
|
||||||
|
let output_x = x0 + x as u16;
|
||||||
let time_1 = time_start + x as usize * time_zoom;
|
let time_1 = time_start + x as usize * time_zoom;
|
||||||
let time_2 = time_1 + time_zoom;
|
let time_2 = time_1 + time_zoom;
|
||||||
if time_1 <= time_point && time_point < time_2 {
|
if time_1 <= time_point && time_point < time_2 {
|
||||||
to.blit(&"█", x0 + x as u16, y0 + y as u16, style);
|
to.blit(&"█", output_x, output_y, style);
|
||||||
let tail = note_len as u16 / time_zoom as u16;
|
let tail = note_len as u16 / time_zoom as u16;
|
||||||
for x_tail in (x0 + x + 1)..(x0 + x + tail) {
|
for x_tail in (output_x + 1)..(output_x + tail) {
|
||||||
to.blit(&"▂", x_tail, y0 + y as u16, style);
|
to.blit(&"▂", x_tail, output_y, style);
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -160,38 +162,60 @@ pub struct PianoHorizontalNotes<'a> {
|
||||||
}
|
}
|
||||||
render!(|self: PianoHorizontalNotes<'a>|render(|to|Ok({
|
render!(|self: PianoHorizontalNotes<'a>|render(|to|Ok({
|
||||||
let note_hi = self.range.note_hi();
|
let note_hi = self.range.note_hi();
|
||||||
|
let note_lo = self.range.note_lo();
|
||||||
let time_start = self.range.time_start();
|
let time_start = self.range.time_start();
|
||||||
let source = &self.buffer;
|
let source = &self.buffer;
|
||||||
let [x0, y0, w, h] = to.area().xywh();
|
let [x0, y0, w, h] = to.area().xywh();
|
||||||
let debug = false;
|
if h as usize != self.range.note_axis() {
|
||||||
|
panic!("area height mismatch");
|
||||||
|
}
|
||||||
|
for (input_x, output_x) in (x0..x0+w).enumerate() {
|
||||||
|
for (input_y, output_y, note) in (note_lo..note_hi).rev().enumerate().map(|(y, n)|(y, y0 + y as u16, n)) {
|
||||||
|
if input_x % 10 == 0 {
|
||||||
|
to.blit(&format!("{input_y} {note}"), output_x, output_y, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//for (input_y, output_y) in (y0..=y0+h).enumerate() {
|
||||||
|
//if note_hi < input_y {
|
||||||
|
//continue
|
||||||
|
//}
|
||||||
|
//if note_hi > 127 {
|
||||||
|
//continue
|
||||||
|
//}
|
||||||
|
//let note = note_hi - input_y;
|
||||||
|
//if input_x % 4 == 0 {
|
||||||
|
//to.blit(&format!("{note}"), output_x, output_y, None);
|
||||||
|
//}
|
||||||
|
////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);
|
||||||
|
////let in_x = source_x < source.width;
|
||||||
|
////let in_y = source_y < source.height;
|
||||||
|
////if in_x && in_y {
|
||||||
|
////let output_cell = to.buffer.get_mut(output_x, output_y);
|
||||||
|
////if let Some(source_cell) = source.get(source_x, source_y) {
|
||||||
|
//*output_cell = source_cell.clone();
|
||||||
|
////}
|
||||||
|
////}
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
let debug = true;
|
||||||
if debug {
|
if debug {
|
||||||
to.blit(&format!("KYP"), x0, y0, None);
|
let x0=20+x0;
|
||||||
to.blit(&format!("x0={x0}"), x0, y0+1, None);
|
to.blit(&format!("KYP "), x0, y0, None);
|
||||||
to.blit(&format!("y0={y0}"), x0, y0+2, None);
|
to.blit(&format!("x0={x0} "), x0, y0+1, None);
|
||||||
to.blit(&format!("note_lo={note_hi}"), x0, y0+3, None);
|
to.blit(&format!("y0={y0} "), x0, y0+2, None);
|
||||||
to.blit(&format!("time_start={time_start}"), x0, y0+4, None);
|
to.blit(&format!("note_lo={note_lo}, {} ", to_note_name(note_lo)), x0, y0+3, None);
|
||||||
|
to.blit(&format!("note_hi={note_hi}, {} ", to_note_name(note_hi)), x0, y0+4, None);
|
||||||
|
to.blit(&format!("time_start={time_start} "), x0, y0+5, None);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
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);
|
|
||||||
let in_x = source_x < source.width;
|
|
||||||
let in_y = source_y < source.height;
|
|
||||||
if in_x && in_y {
|
|
||||||
let target_cell = to.buffer.get_mut(target_x, target_y);
|
|
||||||
if let Some(source_cell) = source.get(source_x, source_y) {
|
|
||||||
*target_cell = source_cell.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})));
|
})));
|
||||||
|
|
||||||
impl PianoHorizontal {
|
impl PianoHorizontal {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue