mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 12:16:42 +01:00
435 lines
18 KiB
Rust
435 lines
18 KiB
Rust
use crate::*;
|
|
impl Content for Sequencer<Tui> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
Stack::down(move|add|{
|
|
add(&self.transport)?;
|
|
add(&self.phrases.clone()
|
|
.split(Direction::Right, 20, &self.editor as &dyn Widget<Engine = Tui>)
|
|
.min_y(20))
|
|
})
|
|
}
|
|
}
|
|
/// Handle top-level events in standalone arranger.
|
|
impl Handle<Tui> for Sequencer<Tui> {
|
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
if !match self.focused() {
|
|
SequencerFocus::Transport => self.transport.handle(from)?,
|
|
SequencerFocus::PhrasePool => self.phrases.handle(from)?,
|
|
SequencerFocus::PhraseEditor => self.editor.handle(from)?
|
|
}.unwrap_or(false) {
|
|
match from.event() {
|
|
// Tab navigation
|
|
key!(KeyCode::Tab) => { self.focus_next(); },
|
|
key!(Shift-KeyCode::Tab) => { self.focus_prev(); },
|
|
key!(KeyCode::BackTab) => { self.focus_prev(); },
|
|
key!(Shift-KeyCode::BackTab) => { self.focus_prev(); },
|
|
// Directional navigation
|
|
key!(KeyCode::Up) => { self.focus_up(); },
|
|
key!(KeyCode::Down) => { self.focus_down(); },
|
|
key!(KeyCode::Left) => { self.focus_left(); },
|
|
key!(KeyCode::Right) => { self.focus_right(); },
|
|
// Global play/pause binding
|
|
key!(KeyCode::Char(' ')) => match self.transport {
|
|
Some(ref mut transport) => { transport.write().unwrap().toggle_play()?; },
|
|
None => { return Ok(None) }
|
|
},
|
|
_ => {}
|
|
}
|
|
};
|
|
Ok(Some(true))
|
|
}
|
|
}
|
|
// TODO: Display phrases always in order of appearance
|
|
impl Content for PhrasePool<Tui> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
let content = col!(
|
|
(i, phrase) in self.phrases.iter().enumerate() => Layers::new(|add|{
|
|
let Phrase { ref name, color, .. } = *phrase.read().unwrap();
|
|
let row1 = format!(" {i}");
|
|
let row2 = if let Some(PhrasePoolMode::Rename(phrase, _)) = self.mode {
|
|
if self.focused && i == phrase {
|
|
format!(" {}▄", name)
|
|
} else {
|
|
format!(" {}", name)
|
|
}
|
|
} else {
|
|
format!(" {}", name)
|
|
};
|
|
add(&col!(row1, row2).fill_x().bg(if i == self.phrase {
|
|
color //Color::Rgb(40, 50, 30)
|
|
} else {
|
|
color //Color::Rgb(28, 35, 25)
|
|
}))?;
|
|
if self.focused && i == self.phrase { add(&CORNERS)?; }
|
|
Ok(())
|
|
})
|
|
)
|
|
.fill_xy()
|
|
.bg(Color::Rgb(28, 35, 25))
|
|
.border(Lozenge(Style::default()
|
|
.bg(Color::Rgb(40, 50, 30))
|
|
.fg(if self.focused {
|
|
Color::Rgb(100, 110, 40)
|
|
} else {
|
|
Color::Rgb(70, 80, 50)
|
|
})));
|
|
lay!(content, TuiStyle::fg("Phrases", if self.focused {
|
|
Color::Rgb(150, 160, 90)
|
|
} else {
|
|
Color::Rgb(120, 130, 100)
|
|
}).push_x(1))
|
|
}
|
|
}
|
|
impl Handle<Tui> for PhrasePool<Tui> {
|
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
match self.mode {
|
|
Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) => {
|
|
let mut phrase = self.phrases[phrase].write().unwrap();
|
|
match from.event() {
|
|
key!(KeyCode::Backspace) => { phrase.name.pop(); },
|
|
key!(KeyCode::Char(c)) => { phrase.name.push(*c); },
|
|
key!(Shift-KeyCode::Char(c)) => { phrase.name.push(*c); },
|
|
key!(KeyCode::Esc) => { phrase.name = old_name.clone(); self.mode = None; },
|
|
key!(KeyCode::Enter) => { self.mode = None; },
|
|
_ => return Ok(None)
|
|
}
|
|
},
|
|
None => match from.event() {
|
|
key!(KeyCode::Up) => self.phrase = if self.phrase > 0 {
|
|
self.phrase - 1
|
|
} else {
|
|
self.phrases.len() - 1
|
|
},
|
|
key!(KeyCode::Down) => {
|
|
self.phrase = (self.phrase + 1) % self.phrases.len()
|
|
},
|
|
key!(KeyCode::Char('a')) => { // append new
|
|
let mut phrase = Phrase::default();
|
|
phrase.name = String::from("(no name)");
|
|
phrase.color = random_color();
|
|
self.phrases.push(Arc::new(RwLock::new(phrase)));
|
|
self.phrase = self.phrases.len() - 1;
|
|
},
|
|
key!(KeyCode::Char('i')) => { // insert new
|
|
let mut phrase = Phrase::default();
|
|
phrase.name = String::from("(no name)");
|
|
phrase.color = random_color();
|
|
self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase)));
|
|
self.phrase += 1;
|
|
},
|
|
key!(KeyCode::Char('d')) => { // insert duplicate
|
|
let mut phrase = (*self.phrases[self.phrase].read().unwrap()).clone();
|
|
phrase.color = random_color_near(phrase.color, 0.1);
|
|
self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase)));
|
|
self.phrase += 1;
|
|
},
|
|
key!(KeyCode::Char('c')) => { // change color
|
|
let mut phrase = self.phrases[self.phrase].write().unwrap();
|
|
phrase.color = random_color();
|
|
},
|
|
key!(KeyCode::Char('n')) => { // change name
|
|
let phrase = self.phrases[self.phrase].read().unwrap();
|
|
self.mode = Some(PhrasePoolMode::Rename(self.phrase, phrase.name.clone()));
|
|
},
|
|
_ => return Ok(None),
|
|
}
|
|
}
|
|
|
|
return Ok(Some(true))
|
|
}
|
|
}
|
|
impl Content for PhraseEditor<Tui> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
let field_bg = Color::Rgb(28, 35, 25);
|
|
let toolbar = Stack::down(move|add|{
|
|
//let name = format!("{:>9}", self.name.read().unwrap().as_str());
|
|
//add(&col!("Track:", TuiStyle::bg(name.as_str(), field_bg)))?;
|
|
if let Some(phrase) = &self.phrase {
|
|
let phrase = phrase.read().unwrap();
|
|
let length = format!("{}q{}p", phrase.length / PPQ, phrase.length % PPQ);
|
|
let length = format!("{:>9}", &length);
|
|
let loop_on = format!("{:>9}", if phrase.loop_on { "on" } else { "off" });
|
|
let loop_start = format!("{:>9}", phrase.loop_start);
|
|
let loop_end = format!("{:>9}", phrase.loop_length);
|
|
add(&"")?;
|
|
add(&col!("Length:", TuiStyle::bg(length.as_str(), field_bg)))?;
|
|
add(&col!("Loop:", TuiStyle::bg(loop_on.as_str(), field_bg)))?;
|
|
add(&col!("L. start:", TuiStyle::bg(loop_start.as_str(), field_bg)))?;
|
|
add(&col!("L. length:", TuiStyle::bg(loop_end.as_str(), field_bg)))?;
|
|
}
|
|
Ok(())
|
|
}).min_x(10);
|
|
let piano_roll = lay!(
|
|
// keys
|
|
CustomWidget::new(|_|Ok(Some([32u16,4u16])), |to: &mut TuiOutput|{
|
|
if to.area().h() < 2 { return Ok(()) }
|
|
Ok(to.buffer_update(to.area().set_w(5).shrink_y(2), &|cell, x, y|{
|
|
let y = y + self.note_axis.start as u16;
|
|
if x < self.keys.area.width && y < self.keys.area.height {
|
|
*cell = self.keys.get(x, y).clone()
|
|
}
|
|
}))
|
|
}).fill_y(),
|
|
// playhead
|
|
CustomWidget::new(|_|Ok(Some([32u16,2u16])), |to: &mut TuiOutput|{
|
|
if let Some(phrase) = &self.phrase {
|
|
let time_0 = self.time_axis.start;
|
|
let time_z = self.time_axis.scale;
|
|
let now = 0; // TODO FIXME: self.now % phrase.read().unwrap().length;
|
|
let [x, y, width, _] = to.area();
|
|
let x2 = x as usize + Self::H_KEYS_OFFSET;
|
|
let x3 = x as usize + width as usize;
|
|
for x in x2..x3 {
|
|
let step = (time_0 + x2) * time_z;
|
|
let next_step = (time_0 + x2 + 1) * time_z;
|
|
to.blit(&"-", x as u16, y, Some(PhraseEditor::<Tui>::style_timer_step(
|
|
now, step as usize, next_step as usize
|
|
)));
|
|
}
|
|
}
|
|
Ok(())
|
|
}).fill_x(),
|
|
// notes
|
|
CustomWidget::new(|_|Ok(Some([32u16,4u16])), |to: &mut TuiOutput|{
|
|
let offset = Self::H_KEYS_OFFSET as u16;
|
|
if to.area().h() < 2 || to.area().w() < offset { return Ok(()) }
|
|
let area = to.area().push_x(offset).shrink_x(offset).shrink_y(2);
|
|
Ok(to.buffer_update(area, &move |cell, x, y|{
|
|
cell.set_bg(Color::Rgb(20, 20, 20));
|
|
let src_x = ((x as usize + self.time_axis.start) * self.time_axis.scale) as usize;
|
|
let src_y = (y as usize + self.note_axis.start) as usize;
|
|
if src_x < self.buffer.width && src_y < self.buffer.height - 1 {
|
|
let src = self.buffer.get(src_x, self.buffer.height - src_y);
|
|
src.map(|src|{ cell.set_symbol(src.symbol()); cell.set_fg(src.fg); });
|
|
}
|
|
}))
|
|
}).fill_x(),
|
|
// note cursor
|
|
CustomWidget::new(|_|Ok(Some([1u16,1u16])), |to: &mut TuiOutput|{
|
|
let area = to.area();
|
|
if let (Some(time), Some(note)) = (self.time_axis.point, self.note_axis.point) {
|
|
let x = area.x() + Self::H_KEYS_OFFSET as u16 + time as u16;
|
|
let y = area.y() + 1 + note as u16 / 2;
|
|
let c = if note % 2 == 0 { "▀" } else { "▄" };
|
|
to.blit(&c, x, y, self.style_focus());
|
|
}
|
|
Ok(())
|
|
}),
|
|
// zoom
|
|
CustomWidget::new(|_|Ok(Some([10u16,1u16])), |to: &mut TuiOutput|{
|
|
let [x, y, w, h] = to.area.xywh();
|
|
let quant = ppq_to_name(self.time_axis.scale);
|
|
Ok(to.blit(
|
|
&quant,
|
|
x + w - 1 - quant.len() as u16,
|
|
y + h - 2,
|
|
self.style_focus()
|
|
))
|
|
}),
|
|
);
|
|
let content = row!(toolbar, piano_roll)
|
|
.fill_x()
|
|
.bg(Color::Rgb(40, 50, 30))
|
|
.border(Lozenge(Style::default()
|
|
.bg(Color::Rgb(40, 50, 30))
|
|
.fg(if self.focused {
|
|
Color::Rgb(100, 110, 40)
|
|
} else {
|
|
Color::Rgb(70, 80, 50)
|
|
})));
|
|
lay!(content, TuiStyle::fg("Sequencer", if self.focused {
|
|
Color::Rgb(150, 160, 90)
|
|
} else {
|
|
Color::Rgb(120, 130, 100)
|
|
}).push_x(1))
|
|
}
|
|
}
|
|
impl Handle<Tui> for PhraseEditor<Tui> {
|
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
match from.event() {
|
|
key!(KeyCode::Char('`')) => {
|
|
self.mode = !self.mode;
|
|
},
|
|
key!(KeyCode::Up) => match self.entered {
|
|
true => { self.note_axis.point_dec(); },
|
|
false => { self.note_axis.start_dec(); },
|
|
},
|
|
key!(KeyCode::Down) => match self.entered {
|
|
true => { self.note_axis.point_inc(); },
|
|
false => { self.note_axis.start_inc(); },
|
|
},
|
|
key!(KeyCode::Left) => match self.entered {
|
|
true => { self.time_axis.point_dec(); },
|
|
false => { self.time_axis.start_dec(); },
|
|
},
|
|
key!(KeyCode::Right) => match self.entered {
|
|
true => { self.time_axis.point_inc(); },
|
|
false => { self.time_axis.start_inc(); },
|
|
},
|
|
_ => {
|
|
return Ok(None)
|
|
}
|
|
}
|
|
return Ok(Some(true))
|
|
}
|
|
}
|
|
impl PhraseEditor<Tui> {
|
|
const H_KEYS_OFFSET: usize = 5;
|
|
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
|
|
pub fn show (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
|
|
if let Some(phrase) = phrase {
|
|
self.phrase = Some(phrase.clone());
|
|
let width = usize::MAX.min(phrase.read().unwrap().length);
|
|
let mut buffer = BigBuffer::new(width, 64);
|
|
let phrase = phrase.read().unwrap();
|
|
Self::fill_seq_bg(&mut buffer, phrase.length, phrase.ppq);
|
|
Self::fill_seq_fg(&mut buffer, &phrase);
|
|
self.buffer = buffer;
|
|
} else {
|
|
self.phrase = None;
|
|
self.buffer = Default::default();
|
|
}
|
|
}
|
|
fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) {
|
|
for x in 0..buf.width {
|
|
if x as usize >= length {
|
|
break
|
|
}
|
|
let style = Style::default();
|
|
buf.get_mut(x, 0).map(|cell|{
|
|
cell.set_char('-');
|
|
cell.set_style(style);
|
|
});
|
|
for y in 0 .. buf.height {
|
|
buf.get_mut(x, y).map(|cell|{
|
|
cell.set_char(if ppq == 0 {
|
|
'·'
|
|
} else if x % (4 * ppq) == 0 {
|
|
'│'
|
|
} else if x % ppq == 0 {
|
|
'╎'
|
|
} else {
|
|
'·'
|
|
});
|
|
cell.set_fg(Color::Gray);
|
|
cell.modifier = Modifier::DIM;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) {
|
|
let mut notes_on = [false;128];
|
|
for x in 0..buf.width {
|
|
if x as usize >= phrase.length {
|
|
break
|
|
}
|
|
if let Some(notes) = phrase.notes.get(x as usize) {
|
|
if phrase.percussive {
|
|
for note in notes {
|
|
match note {
|
|
MidiMessage::NoteOn { key, .. } =>
|
|
notes_on[key.as_int() as usize] = true,
|
|
_ => {}
|
|
}
|
|
}
|
|
} else {
|
|
for note in notes {
|
|
match note {
|
|
MidiMessage::NoteOn { key, .. } =>
|
|
notes_on[key.as_int() as usize] = true,
|
|
MidiMessage::NoteOff { key, .. } =>
|
|
notes_on[key.as_int() as usize] = false,
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
for y in 0..buf.height/2 {
|
|
if y >= 64 {
|
|
break
|
|
}
|
|
if let Some(block) = half_block(
|
|
notes_on[y as usize * 2],
|
|
notes_on[y as usize * 2 + 1],
|
|
) {
|
|
buf.get_mut(x, y).map(|cell|{
|
|
cell.set_char(block);
|
|
cell.set_fg(Color::White);
|
|
});
|
|
}
|
|
}
|
|
if phrase.percussive {
|
|
notes_on.fill(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
pub(crate) fn style_focus (&self) -> Option<Style> {
|
|
Some(if self.focused {
|
|
Style::default().green().not_dim()
|
|
} else {
|
|
Style::default().green().dim()
|
|
})
|
|
}
|
|
pub(crate) fn style_timer_step (now: usize, step: usize, next_step: usize) -> Style {
|
|
if step <= now && now < next_step {
|
|
Style::default().yellow().bold().not_dim()
|
|
} else {
|
|
Style::default()
|
|
}
|
|
}
|
|
pub fn index_to_color (&self, index: u16, default: Color) -> Color {
|
|
let index = index as usize;
|
|
let (notes_in, notes_out) = (self.notes_in.read().unwrap(), self.notes_out.read().unwrap());
|
|
if notes_in[index] && notes_out[index] {
|
|
Color::Yellow
|
|
} else if notes_in[index] {
|
|
Color::Red
|
|
} else if notes_out[index] {
|
|
Color::Green
|
|
} else {
|
|
default
|
|
}
|
|
}
|
|
}
|
|
/// Colors of piano keys
|
|
const KEY_COLORS: [(Color, Color);6] = [
|
|
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
|
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
|
(Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)),
|
|
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
|
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
|
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
|
];
|
|
pub(crate) fn keys_vert () -> Buffer {
|
|
let area = [0, 0, 5, 64];
|
|
let mut buffer = Buffer::empty(Rect {
|
|
x: area.x(), y: area.y(), width: area.w(), height: area.h()
|
|
});
|
|
buffer_update(&mut buffer, area, &|cell, x, y| {
|
|
let y = 63 - y;
|
|
match x {
|
|
0 => {
|
|
cell.set_char('▀');
|
|
let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize];
|
|
cell.set_fg(fg);
|
|
cell.set_bg(bg);
|
|
},
|
|
1 => {
|
|
cell.set_char('▀');
|
|
cell.set_fg(Color::White);
|
|
cell.set_bg(Color::White);
|
|
},
|
|
2 => if y % 6 == 0 { cell.set_char('C'); },
|
|
3 => if y % 6 == 0 { cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]); },
|
|
_ => {}
|
|
}
|
|
});
|
|
buffer
|
|
}
|
|
/// Octave number (from -1 to 9)
|
|
const NTH_OCTAVE: [&'static str;11] = [
|
|
"-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
|
|
];
|