wip: sequencer now copies from buffers

This commit is contained in:
🪞👃🪞 2024-07-13 21:57:07 +03:00
parent aa478099d9
commit 2fc8e84551
15 changed files with 310 additions and 256 deletions

View file

@ -4,3 +4,6 @@ status:
cargo c
cloc --by-file src/
git status
push:
git push -u codeberg main
git push -u origin main

View file

@ -78,11 +78,11 @@ pub const KEYMAP_GLOBAL: &'static [KeyBinding<App>] = keymap!(App {
Ok(true)
}],
[Char('='), NONE, "zoom_in", "show fewer ticks per block", |app: &mut App| {
app.sequencer.time_zoom = prev_note_length(app.sequencer.time_zoom);
app.sequencer.time_axis.scale_mut(&prev_note_length);
Ok(true)
}],
[Char('-'), NONE, "zoom_out", "show more ticks per block", |app: &mut App| {
app.sequencer.time_zoom = next_note_length(app.sequencer.time_zoom);
app.sequencer.time_axis.scale_mut(&next_note_length);
Ok(true)
}],
[Char('x'), NONE, "extend", "double the current clip", |app: &mut App| {

View file

@ -11,7 +11,7 @@ pub const KEYMAP_ARRANGER: &'static [KeyBinding<App>] = keymap!(App {
false => app.arranger.scene_prev(),
true => app.arranger.track_prev(),
};
app.sequencer.phrase = app.arranger.phrase().map(Clone::clone);
app.sequencer.show(app.arranger.phrase())?;
Ok(true)
}],
[Down, NONE, "arranger_cursor_down", "move cursor down", |app: &mut App| {
@ -19,7 +19,7 @@ pub const KEYMAP_ARRANGER: &'static [KeyBinding<App>] = keymap!(App {
false => app.arranger.scene_next(),
true => app.arranger.track_next(),
};
app.sequencer.phrase = app.arranger.phrase().map(Clone::clone);
app.sequencer.show(app.arranger.phrase())?;
Ok(true)
}],
[Left, NONE, "arranger_cursor_left", "move cursor left", |app: &mut App| {
@ -27,7 +27,7 @@ pub const KEYMAP_ARRANGER: &'static [KeyBinding<App>] = keymap!(App {
false => app.arranger.track_prev(),
true => app.arranger.scene_prev(),
};
app.sequencer.phrase = app.arranger.phrase().map(Clone::clone);
app.sequencer.show(app.arranger.phrase())?;
Ok(true)
}],
[Right, NONE, "arranger_cursor_right", "move cursor right", |app: &mut App| {
@ -35,7 +35,7 @@ pub const KEYMAP_ARRANGER: &'static [KeyBinding<App>] = keymap!(App {
false => app.arranger.track_next(),
true => app.arranger.scene_next()
};
app.sequencer.phrase = app.arranger.phrase().map(Clone::clone);
app.sequencer.show(app.arranger.phrase())?;
Ok(true)
}],
[Char('.'), NONE, "arranger_increment", "set next clip at cursor", |app: &mut App| {

View file

@ -3,26 +3,30 @@ use crate::{core::*, model::App};
/// Key bindings for phrase editor.
pub const KEYMAP_SEQUENCER: &'static [KeyBinding<App>] = keymap!(App {
[Up, NONE, "seq_cursor_up", "move cursor up", |app: &mut App| {
app.sequencer.note_cursor = app.sequencer.note_cursor.saturating_sub(1);
match app.sequencer.entered {
true => { app.sequencer.note_axis.point_dec(); },
false => { app.sequencer.note_axis.start_dec(); },
}
Ok(true)
}],
[Down, NONE, "seq_cursor_down", "move cursor up", |app: &mut App| {
app.sequencer.note_cursor = app.sequencer.note_cursor + 1;
[Down, NONE, "seq_cursor_down", "move cursor down", |app: &mut App| {
match app.sequencer.entered {
true => { app.sequencer.note_axis.point_inc(); },
false => { app.sequencer.note_axis.start_inc(); },
}
Ok(true)
}],
[Left, NONE, "seq_cursor_left", "move cursor up", |app: &mut App| {
if app.sequencer.entered {
app.sequencer.time_cursor = app.sequencer.time_cursor.saturating_sub(1);
} else {
app.sequencer.time_start = app.sequencer.time_start.saturating_sub(1);
match app.sequencer.entered {
true => { app.sequencer.time_axis.point_dec(); },
false => { app.sequencer.time_axis.start_dec(); },
}
Ok(true)
}],
[Right, NONE, "seq_cursor_right", "move cursor up", |app: &mut App| {
if app.sequencer.entered {
app.sequencer.time_cursor = app.sequencer.time_cursor + 1;
} else {
app.sequencer.time_start = app.sequencer.time_start + 1;
match app.sequencer.entered {
true => { app.sequencer.time_axis.point_inc(); },
false => { app.sequencer.time_axis.start_inc(); },
}
Ok(true)
}],
@ -35,4 +39,3 @@ pub const KEYMAP_SEQUENCER: &'static [KeyBinding<App>] = keymap!(App {
// [CapsLock, NONE, "advance", "Toggle auto advance", nop],
// [Char('w'), NONE, "rest", "Advance by note duration", nop],
});

View file

@ -80,7 +80,7 @@ pub fn run <T> (state: Arc<RwLock<T>>) -> Usually<Arc<RwLock<T>>>
terminal_setup()?;
panic_hook_setup();
let main_thread = main_thread(&exited, &state)?;
main_thread.join().unwrap();
main_thread.join().expect("main thread failed");
terminal_teardown()?;
Ok(state)
}

View file

@ -33,8 +33,8 @@ pub fn write_midi_output (writer: &mut ::jack::MidiWriter, output: &MIDIChunk, f
}
}
/// (ppq, name)
pub const NOTE_DURATIONS: [(usize, &str);26] = [
/// (pulses, name)
pub const NOTE_DURATIONS: [(u16, &str);26] = [
(1, "1/384"),
(2, "1/192"),
(3, "1/128"),
@ -63,16 +63,7 @@ pub const NOTE_DURATIONS: [(usize, &str);26] = [
(6144, "16/1"),
];
pub fn ppq_to_name (ppq: usize) -> &'static str {
for (length, name) in &NOTE_DURATIONS {
if *length == ppq {
return name
}
}
""
}
pub fn prev_note_length (ppq: usize) -> usize {
pub fn prev_note_length (ppq: u16) -> u16 {
for i in 1..=16 {
let length = NOTE_DURATIONS[16-i].0;
if length < ppq {
@ -82,7 +73,7 @@ pub fn prev_note_length (ppq: usize) -> usize {
ppq
}
pub fn next_note_length (ppq: usize) -> usize {
pub fn next_note_length (ppq: u16) -> u16 {
for (length, _) in &NOTE_DURATIONS {
if *length > ppq {
return *length
@ -90,3 +81,12 @@ pub fn next_note_length (ppq: usize) -> usize {
}
ppq
}
pub fn ppq_to_name (ppq: u16) -> &'static str {
for (length, name) in &NOTE_DURATIONS {
if *length == ppq {
return name
}
}
""
}

View file

@ -2,49 +2,35 @@ use crate::core::*;
pub(crate) use ratatui::prelude::CrosstermBackend;
pub(crate) use ratatui::style::{Stylize, Style, Color};
pub(crate) use ratatui::layout::Rect;
pub(crate) use ratatui::buffer::Buffer;
pub(crate) use ratatui::buffer::{Buffer, Cell};
use ratatui::widgets::WidgetRef;
pub fn buffer_update (
buffer: &mut Buffer, area: Rect, callback: &impl Fn(&mut Cell, u16, u16)
) {
for row in 0..area.height {
let y = area.y + row;
for col in 0..area.width {
let x = area.x + col;
callback(buffer.get_mut(x, y), col, row);
}
}
}
pub fn fill_fg (buf: &mut Buffer, area: Rect, color: Color) {
let Rect { x, y, width, height } = area;
for y in y..y+height {
if y >= buf.area.height {
break
}
for x in x..x+width {
if x >= buf.area.width {
break
}
buf.get_mut(x, y).set_fg(color);
}
}
buffer_update(buf, area, &|cell,_,_|{cell.set_fg(color);})
}
pub fn fill_bg (buf: &mut Buffer, area: Rect, color: Color) {
let Rect { x, y, width, height } = area;
for y in y..y+height {
if y >= buf.area.height {
break
}
for x in x..x+width {
if x >= buf.area.width {
break
}
buf.get_mut(x, y).set_bg(color);
}
}
buffer_update(buf, area, &|cell,_,_|{cell.set_bg(color);})
}
pub fn fill_char (buf: &mut Buffer, area: Rect, c: char) {
let Rect { x, y, width, height } = area;
for y in y..y+height {
if y >= buf.area.height {
break
}
for x in x..x+width {
if x >= buf.area.width {
break
}
buf.get_mut(x, y).set_char(c);
}
buffer_update(buf, area, &|cell,_,_|{cell.set_char(c);})
}
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
match (lower, upper) {
(true, true) => Some('█'),
(true, false) => Some('▄'),
(false, true) => Some('▀'),
_ => None
}
}

View file

@ -1,6 +1,6 @@
//! Application state.
submod! { arranger looper mixer phrase plugin sampler sequencer scene track transport }
submod! { axis arranger looper mixer phrase plugin sampler sequencer scene track transport }
use crate::core::*;
@ -71,7 +71,7 @@ process!(App |self, _client, scope| {
&self.transport.timebase,
self.transport.playing,
self.transport.started,
self.transport.quant,
self.transport.quant as usize,
reset,
&scope,
(current_frames as usize, self.chunk_size),

33
src/model/axis.rs Normal file
View file

@ -0,0 +1,33 @@
macro_rules! impl_axis_common { ($A:ident $T:ty) => {
impl $A<$T> {
pub fn start_inc (&mut self) -> $T {
self.start = self.start + 1;
self.start
}
pub fn start_dec (&mut self) -> $T {
self.start = self.start.saturating_sub(1);
self.start
}
pub fn point_inc (&mut self) -> Option<$T> {
self.point = self.point.map(|p|p + 1);
self.point
}
pub fn point_dec (&mut self) -> Option<$T> {
self.point = self.point.map(|p|p.saturating_sub(1));
self.point
}
}
} }
pub struct FixedAxis<T> { pub start: T, pub point: Option<T> }
impl_axis_common!(FixedAxis u16);
impl_axis_common!(FixedAxis usize);
pub struct ScaledAxis<T> { pub start: T, pub scale: T, pub point: Option<T> }
impl_axis_common!(ScaledAxis u16);
impl_axis_common!(ScaledAxis usize);
impl<T: Copy> ScaledAxis<T> {
pub fn scale_mut (&mut self, cb: &impl Fn(T)->T) {
self.scale = cb(self.scale)
}
}

View file

@ -19,7 +19,9 @@ pub struct Phrase {
pub name: String,
pub length: usize,
pub notes: PhraseData,
pub looped: Option<(usize, usize)>
pub looped: Option<(usize, usize)>,
/// Immediate note-offs in view
pub percussive: bool
}
impl Default for Phrase {
@ -34,7 +36,8 @@ impl Phrase {
name: name.to_string(),
length,
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
looped: Some((0, length))
looped: Some((0, length)),
percussive: true,
}
}
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {

View file

@ -1,46 +1,160 @@
use crate::{core::*, model::Phrase};
use crate::{core::*, model::{FixedAxis, ScaledAxis, Phrase}};
pub struct Sequencer {
pub phrase: Option<Arc<RwLock<Phrase>>>,
pub mode: bool,
pub buffer: Buffer,
pub now: usize,
pub ppq: usize,
pub note_cursor: usize,
pub note_start: usize,
pub time_cursor: usize,
pub time_start: usize,
pub time_zoom: usize,
pub focused: bool,
pub entered: bool,
pub phrase: Option<Arc<RwLock<Phrase>>>,
pub buffer: Buffer,
pub keys: Buffer,
/// Highlight input keys
pub notes_in: [bool; 128],
pub keys_in: [bool; 128],
/// Highlight output keys
pub notes_out: [bool; 128],
pub keys_out: [bool; 128],
pub now: usize,
pub ppq: usize,
pub note_axis: FixedAxis<u16>,
pub time_axis: ScaledAxis<u16>,
}
impl Sequencer {
pub fn new () -> Self {
Self {
buffer: Buffer::empty(Rect::default()),
keys: keys_vert(),
entered: false,
focused: false,
mode: false,
note_cursor: 0,
note_start: 0,
notes_in: [false;128],
notes_out: [false;128],
keys_in: [false;128],
keys_out: [false;128],
phrase: None,
time_cursor: 0,
time_start: 0,
time_zoom: 12,
now: 0,
ppq: 96
ppq: 96,
note_axis: FixedAxis {
start: 12,
point: Some(36)
},
time_axis: ScaledAxis {
start: 0,
scale: 24,
point: Some(0)
},
}
}
pub fn show (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
/// FIXME: Support phrases longer that 65536 ticks
pub fn show (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) -> Usually<()> {
self.phrase = phrase.map(Clone::clone);
if let Some(ref phrase) = self.phrase {
let width = u16::MAX.min(phrase.read().unwrap().length as u16);
let mut buffer = Buffer::empty(Rect { x: 0, y: 0, width, height: 64 });
let phrase = phrase.read().unwrap();
fill_seq_bg(&mut buffer, phrase.length, self.ppq)?;
fill_seq_fg(&mut buffer, &phrase)?;
self.buffer = buffer;
} else {
self.buffer = Buffer::empty(Rect::default())
}
Ok(())
}
}
fn keys_vert () -> Buffer {
let area = Rect { x: 0, y: 0, width: 5, height: 64 };
let mut buffer = Buffer::empty(area);
buffer_update(&mut buffer, area, &|cell, x, y| {
cell.set_char('▀');
match x {
0 => {
let (fg, bg) = key_colors(y);
cell.set_fg(fg);
cell.set_bg(bg);
},
1 => {
cell.set_fg(Color::White);
cell.set_bg(Color::White);
}
_ => {}
}
});
buffer
}
fn key_colors (index: u16) -> (Color, Color) {
match index % 6 {
0 => (Color::White, Color::Black),
1 => (Color::White, Color::Black),
2 => (Color::White, Color::White),
3 => (Color::Black, Color::White),
4 => (Color::Black, Color::White),
5 => (Color::Black, Color::White),
_ => unreachable!()
}
}
fn fill_seq_bg (buf: &mut Buffer, length: usize, ppq: usize) -> Usually<()> {
for x in 0 .. buf.area.width - buf.area.x {
if x as usize >= length {
break
}
let style = Style::default();
let cell = buf.get_mut(x, buf.area.y);
cell.set_char('-');
cell.set_style(style);
let character = if ppq > 0 && x as usize % ppq == 0 { '|' } else { '·' };
for y in 0 .. buf.area.height - buf.area.y {
let cell = buf.get_mut(x, y);
cell.set_char(character);
cell.set_fg(Color::Gray);
}
}
Ok(())
}
fn fill_seq_fg (buf: &mut Buffer, phrase: &Phrase) -> Usually<()> {
let mut notes_on = [false;128];
for x in 0 .. buf.area.width - buf.area.x {
if x as usize >= phrase.length {
break
}
if let Some(notes) = phrase.notes.get(x as usize) {
for note in notes {
if phrase.percussive {
match note {
MidiMessage::NoteOn { key, .. } =>
notes_on[key.as_int() as usize] = true,
_ => {}
}
} else {
match note {
MidiMessage::NoteOn { key, .. } =>
notes_on[key.as_int() as usize] = true,
MidiMessage::NoteOff { key, .. } =>
notes_on[key.as_int() as usize] = false,
_ => {}
}
}
}
for y in 0 .. (buf.area.height - buf.area.y) / 2 {
if y >= 64 {
break
}
if let Some(block) = half_block(
notes_on[y as usize * 2],
notes_on[y as usize * 2 + 1],
) {
let cell = buf.get_mut(x, y);
cell.set_char(block);
cell.set_fg(Color::White);
}
}
if phrase.percussive {
notes_on.fill(false);
}
}
}
Ok(())
}

View file

@ -123,7 +123,7 @@ impl Track {
(frame0.saturating_sub(start_frame), frames, period)
);
}
});
}).unwrap();
let mut phrase = phrase.write().unwrap();
let length = phrase.length;
// Monitor and record input

View file

@ -33,9 +33,9 @@ pub struct TransportToolbar {
/// JACK transport handle.
transport: Option<Transport>,
/// Quantization factor
pub quant: usize,
pub quant: u16,
/// Global sync quant
pub sync: usize,
pub sync: u16,
/// Current transport state
pub playing: Option<TransportState>,
/// Current position according to transport
@ -56,7 +56,7 @@ impl TransportToolbar {
playing: Some(TransportState::Stopped),
started: None,
quant: 24,
sync: timebase.ppq() as usize * 4,
sync: timebase.ppq() as u16 * 4,
transport,
timebase,
}

View file

@ -24,12 +24,13 @@ impl Sequencer {
Style::default()
}
}
fn index_to_color (&self, index: usize, default: Color) -> Color {
if self.notes_in[index] && self.notes_out[index] {
fn index_to_color (&self, index: u16, default: Color) -> Color {
let index = index as usize;
if self.keys_in[index] && self.keys_out[index] {
Color::Yellow
} else if self.notes_in[index] {
} else if self.keys_in[index] {
Color::Red
} else if self.notes_out[index] {
} else if self.keys_out[index] {
Color::Green
} else {
default
@ -44,166 +45,77 @@ impl Sequencer {
fn horizontal_draw (&self, buf: &mut Buffer, area: Rect) -> Usually<()> {
self.horizontal_keys(buf, area)?;
self.horizontal_quant(buf, area)?;
self.horizontal_timer(buf, area, &self.phrase)?;
self.horizontal_lanes(buf, area, &self.phrase)?;
if let Some(ref phrase) = self.phrase {
self.horizontal_timer(buf, area, phrase)?;
}
self.horizontal_notes(buf, area)?;
self.horizontal_cursor(buf, area)?;
self.horizontal_quant(buf, area)?;
Ok(())
}
fn horizontal_notes (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
let area = Rect {
x: area.x + Self::H_KEYS_OFFSET,
y: area.y + 1,
width: area.width - Self::H_KEYS_OFFSET,
height: area.height - 1
};
buffer_update(buf, area, &|cell, x, y|{
let src_x = (x + self.time_axis.start) * self.time_axis.scale;
let src_y = y + self.note_axis.start;
if src_x < self.buffer.area.width && src_y < self.buffer.area.height {
let src = self.buffer.get(src_x, src_y);
cell.set_symbol(src.symbol());
cell.set_fg(src.fg);
}
});
Ok(area)
}
fn horizontal_keys (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
let area = Rect {
x: area.x,
y: area.y + 1,
width: 2,
height: area.height - 2
};
buffer_update(buf, area, &|cell, x, y|{
*cell = self.keys.get(x, y % 6).clone()
});
Ok(area)
}
fn horizontal_quant (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
let quant = ppq_to_name(self.time_zoom);
let quant = ppq_to_name(self.time_axis.scale);
let quant_x = area.x + area.width - 1 - quant.len() as u16;
let quant_y = area.y + area.height - 2;
quant.blit(buf, quant_x, quant_y, self.style_focus())
}
fn horizontal_cursor (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
let (time, note) = (self.time_cursor, self.note_cursor);
if let (Some(time), Some(note)) = (self.time_axis.point, self.note_axis.point) {
let x = area.x + Self::H_KEYS_OFFSET + time as u16;
let y = area.y + 1 + note as u16 / 2;
let c = if note % 2 == 0 { "" } else { "" };
c.blit(buf, x, y, self.style_focus())
} else {
Ok(Rect::default())
}
}
fn horizontal_timer (
&self, buf: &mut Buffer, area: Rect, phrase: &Option<Arc<RwLock<Phrase>>>
&self, buf: &mut Buffer, area: Rect, phrase: &RwLock<Phrase>
) -> Usually<Rect> {
if let Some(phrase) = phrase {
let phrase = phrase.read().unwrap();
let (time0, time_z, now) = (self.time_start, self.time_zoom, self.now % phrase.length);
let (time0, time_z, now) = (self.time_axis.start, self.time_axis.scale, self.now % phrase.length);
let Rect { x, width, .. } = area;
for x in x+Self::H_KEYS_OFFSET..x+width {
let step = (time0 + (x-Self::H_KEYS_OFFSET) as usize) * time_z;
let next_step = (time0 + (x-Self::H_KEYS_OFFSET) as usize + 1) * time_z;
let style = Self::style_timer_step(now, step, next_step);
let step = (time0 + (x-Self::H_KEYS_OFFSET)) * time_z;
let next_step = (time0 + (x-Self::H_KEYS_OFFSET) + 1) * time_z;
let style = Self::style_timer_step(now, step as usize, next_step as usize);
"-".blit(buf, x, area.y, Some(style))?;
}
}
return Ok(Rect { x: area.x, y: area.y, width: area.width, height: 1 })
}
fn horizontal_keys (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
let note0 = self.note_start;
let not_dim = Style::default().not_dim();
let Rect { x, y, height, .. } = area;
let height = height.min(128);
let h = height.saturating_sub(3);
for index in 0..h {
let y = y + h - index;
let key1 = buf.get_mut(x + 1, y);
key1.set_char('▄');
key1.set_style(not_dim);
let (fg, bg) = match index % 6 {
0 => (Color::White, Color::Black),
1 => (Color::White, Color::Black),
2 => (Color::White, Color::White),
3 => (Color::Black, Color::White),
4 => (Color::Black, Color::White),
5 => (Color::Black, Color::White),
_ => { unreachable!(); }
};
key1.set_fg(self.index_to_color(index as usize * 2, fg));
key1.set_bg(self.index_to_color(index as usize * 2 + 1, bg));
let key2 = buf.get_mut(x + 2, y);
key2.set_char('▄');
key2.set_style(not_dim);
key2.set_fg(self.index_to_color(index as usize * 2, Color::White));
key2.set_bg(self.index_to_color(index as usize * 2 + 1, Color::White));
//for x in x+Self::H_KEYS_OFFSET..x+width-1 {
//let cell = buf.get_mut(x, y);
//cell.set_char('░');
//cell.set_style(black);
//}
let note_a = note0 + (index * 2) as usize;
if note_a % 12 == 0 {
let octave = format!("C{}", (note_a / 12) as i8 - 2);
octave.blit(buf, x + 3, y, None)?;
continue
}
let note_b = note0 + (index * 2) as usize;
if note_b % 12 == 0 {
let octave = format!("C{}", (note_b / 12) as i8 - 2);
octave.blit(buf, x + 3, y, None)?;
continue
}
}
Ok(area)
}
fn horizontal_lanes (
&self, buf: &mut Buffer, area: Rect, phrase: &Option<Arc<RwLock<Phrase>>>
) -> Usually<Rect> {
if let Some(phrase) = phrase {
let Rect { x, y, width, height } = area;
let phrase = phrase.read().unwrap();
let now = self.now % phrase.length;
let ppq = self.ppq;
let time_z = self.time_zoom;
let time0 = self.time_start;
let note0 = self.note_start;
let dim = Style::default().dim();
let offset = Self::H_KEYS_OFFSET;
let phrase_area = Rect { x: x + offset, y, width: width - offset, height: height - 2 };
let mut steps = Vec::with_capacity(phrase_area.width as usize);
for x in phrase_area.x .. phrase_area.x + phrase_area.width {
let x0 = x.saturating_sub(phrase_area.x) as usize;
let step = (0 + time0 + x0) * time_z;
let next = (1 + time0 + x0) * time_z;
if step >= phrase.length {
break
}
let style = Self::style_timer_step(now, step, next);
let cell = buf.get_mut(x, area.y);
cell.set_char('-');
cell.set_style(style);
steps.push((x, step, next));
for y in phrase_area.y .. phrase_area.y + phrase_area.height {
if y == phrase_area.y {
if step % (4 * ppq) == 0 {
format!("{}", 1 + step / (4 * ppq)).blit(buf, x, y, None)?;
} else if step % ppq == 0 {
let cell = buf.get_mut(x, y);
cell.set_char(if step % ppq == 0 { '|' } else { '·' });
cell.set_style(dim);
}
} else {
let cell = buf.get_mut(x, y);
cell.set_char(if step % ppq == 0 { '|' } else { '·' });
cell.set_style(dim);
}
}
}
let wh = Style::default().white().not_dim();
for index in 0..height-2 {
let note_a = note0 + index as usize * 2;
let note_b = note0 + index as usize * 2 + 1;
for (x, step, next_step) in steps.iter() {
let (a, b) = (
phrase.contains_note_on(u7::from_int_lossy(note_a as u8), *step, *next_step),
phrase.contains_note_on(u7::from_int_lossy(note_b as u8), *step, *next_step),
);
if let Some(block) = half_block(a, b) {
let y = y + height.saturating_sub(index+2) as u16;
let cell = buf.get_mut(*x, y);
cell.set_char(block);
cell.set_style(wh);
}
}
}
Ok(area)
} else {
return Ok(Rect { x: area.x, y: area.x, width: 0, height: 0 })
}
}
}
fn half_block (lower: bool, upper: bool) -> Option<char> {
match (lower, upper) {
(true, true) => Some('█'),
(true, false) => Some('▄'),
(false, true) => Some('▀'),
_ => None
}
}

View file

@ -48,7 +48,7 @@ render!(TransportToolbar |self, buf, area| {
// Quantization
&|buf: &mut Buffer, Rect { x, y, .. }: Rect|{
"QUANT".blit(buf, x, y, Some(not_dim))?;
let width = ppq_to_name(*quant).blit(buf, x, y + 1, Some(not_dim_bold))?.width;
let width = ppq_to_name(*quant as u16).blit(buf, x, y + 1, Some(not_dim_bold))?.width;
let area = Rect { x, y, width: (width + 2).max(10), height: 2 };
if self.focused && self.entered && self.selected == TransportFocus::Quant {
corners.draw(buf, Rect { x: area.x - 1, ..area })?;
@ -59,7 +59,7 @@ render!(TransportToolbar |self, buf, area| {
// Clip launch sync
&|buf: &mut Buffer, Rect { x, y, .. }: Rect|{
"SYNC".blit(buf, x, y, Some(not_dim))?;
let width = ppq_to_name(*sync).blit(buf, x, y + 1, Some(not_dim_bold))?.width;
let width = ppq_to_name(*sync as u16).blit(buf, x, y + 1, Some(not_dim_bold))?.width;
let area = Rect { x, y, width: (width + 2).max(10), height: 2 };
if self.focused && self.entered && self.selected == TransportFocus::Sync {
corners.draw(buf, Rect { x: area.x - 1, ..area })?;