wip: refactor into crates

This commit is contained in:
🪞👃🪞 2024-08-03 21:55:38 +03:00
parent 96e17e7f7c
commit 5ae99b4ada
87 changed files with 2281 additions and 2217 deletions

View file

@ -0,0 +1,144 @@
//! Clip launcher and arrangement editor.
use crate::*;
use self::arr_focus::ArrangerFocus;
pub use self::arr_scene::Scene;
submod! { arr_draw_h arr_draw_v arr_focus arr_phrase arr_scene arr_track }
/// Key bindings for arranger section.
pub const KEYMAP_ARRANGER: &'static [KeyBinding<Arranger>] = keymap!(Arranger {
[Char('`'), NONE, "arranger_mode_switch", "switch the display mode", |app: &mut Arranger| {
app.mode.to_next();
Ok(true)
}],
[Up, NONE, "arranger_cursor_up", "move cursor up", |app: &mut Arranger| {
match app.mode {
ArrangerViewMode::Horizontal => app.track_prev(),
_ => app.scene_prev(),
};
app.show_phrase()?;
Ok(true)
}],
[Down, NONE, "arranger_cursor_down", "move cursor down", |app: &mut Arranger| {
match app.mode {
ArrangerViewMode::Horizontal => app.track_next(),
_ => app.scene_next(),
};
app.show_phrase()?;
Ok(true)
}],
[Left, NONE, "arranger_cursor_left", "move cursor left", |app: &mut Arranger| {
match app.mode {
ArrangerViewMode::Horizontal => app.scene_prev(),
_ => app.track_prev(),
};
app.show_phrase()?;
Ok(true)
}],
[Right, NONE, "arranger_cursor_right", "move cursor right", |app: &mut Arranger| {
match app.mode {
ArrangerViewMode::Horizontal => app.scene_next(),
_ => app.track_next(),
};
app.show_phrase()?;
Ok(true)
}],
[Char('.'), NONE, "arranger_increment", "set next clip at cursor", |app: &mut Arranger| {
app.phrase_next();
app.sequencer.phrase = app.phrase().map(Clone::clone);
Ok(true)
}],
[Char(','), NONE, "arranger_decrement", "set previous clip at cursor", |app: &mut Arranger| {
app.phrase_prev();
app.sequencer.phrase = app.phrase().map(Clone::clone);
Ok(true)
}],
[Enter, NONE, "arranger_activate", "activate item at cursor", |app: &mut Arranger| {
app.activate();
Ok(true)
}],
});
/// Represents the tracks and scenes of the composition.
pub struct Arranger {
/// Display mode of arranger
pub mode: ArrangerViewMode,
/// Currently selected element.
pub selected: ArrangerFocus,
/// Collection of tracks.
pub tracks: Vec<Track>,
/// Collection of scenes.
pub scenes: Vec<Scene>,
pub focused: bool,
pub entered: bool,
pub fixed_height: bool,
pub sequencer: Sequencer,
}
/// Display mode of arranger
pub enum ArrangerViewMode {
Vertical,
VerticalCompact,
Horizontal,
}
impl ArrangerViewMode {
fn to_next (&mut self) {
*self = match self {
Self::Vertical => Self::VerticalCompact,
Self::VerticalCompact => Self::Horizontal,
Self::Horizontal => Self::Vertical,
}
}
}
impl Arranger {
pub fn new () -> Self {
Self {
mode: ArrangerViewMode::Vertical,
selected: ArrangerFocus::Clip(0, 0),
scenes: vec![],
tracks: vec![],
entered: true,
focused: true,
fixed_height: false,
sequencer: Sequencer::new(),
}
}
pub fn activate (&mut self) {
match self.selected {
ArrangerFocus::Scene(s) => {
for (track_index, track) in self.tracks.iter_mut().enumerate() {
track.sequence = self.scenes[s].clips[track_index];
track.reset = true;
}
},
ArrangerFocus::Clip(t, s) => {
self.tracks[t].sequence = self.scenes[s].clips[t];
self.tracks[t].reset = true;
},
_ => {}
}
}
fn show_phrase (&mut self) -> Usually<()> {
let phrase = self.phrase();
self.sequencer.show(phrase)
}
}
render!(Arranger |self, buf, area| match self.mode {
ArrangerViewMode::Horizontal =>
self::arr_draw_h::draw(self, buf, area),
ArrangerViewMode::Vertical =>
self::arr_draw_v::draw_expanded(self, buf, area),
ArrangerViewMode::VerticalCompact =>
self::arr_draw_v::draw_compact(self, buf, area),
});

View file

@ -0,0 +1,98 @@
use crate::*;
#[derive(PartialEq)]
/// Represents the current user selection in the arranger
pub enum ArrangerFocus {
/** The whole mix is selected */
Mix,
/// A track is selected.
Track(usize),
/// A scene is selected.
Scene(usize),
/// A clip (track × scene) is selected.
Clip(usize, usize),
}
/// Focus identification methods
impl ArrangerFocus {
pub fn is_track (&self) -> bool {
match self { Self::Track(_) => true, _ => false }
}
pub fn is_scene (&self) -> bool {
match self { Self::Scene(_) => true, _ => false }
}
pub fn is_clip (&self) -> bool {
match self { Self::Clip(_, _) => true, _ => false }
}
}
/// Track focus methods
impl ArrangerFocus {
pub fn track (&self) -> Option<usize> {
match self {
Self::Clip(t, _) => Some(*t),
Self::Track(t) => Some(*t),
_ => None
}
}
pub fn track_next (&mut self, last_track: usize) {
*self = match self {
Self::Mix => Self::Track(0),
Self::Track(t) => Self::Track(last_track.min(*t + 1)),
Self::Scene(s) => Self::Clip(0, *s),
Self::Clip(t, s) => Self::Clip(last_track.min(*t + 1), *s),
}
}
pub fn track_prev (&mut self) {
*self = match self {
Self::Mix => Self::Mix,
Self::Scene(s) => Self::Scene(*s),
Self::Track(t) => if *t == 0 {
Self::Mix
} else {
Self::Track(*t - 1)
},
Self::Clip(t, s) => if *t == 0 {
Self::Scene(*s)
} else {
Self::Clip(t.saturating_sub(1), *s)
}
}
}
}
/// Scene focus methods
impl ArrangerFocus {
pub fn scene (&self) -> Option<usize> {
match self {
Self::Clip(_, s) => Some(*s),
Self::Scene(s) => Some(*s),
_ => None
}
}
pub fn scene_next (&mut self, last_scene: usize) {
*self = match self {
Self::Mix => Self::Scene(0),
Self::Track(t) => Self::Clip(*t, 0),
Self::Scene(s) => Self::Scene(last_scene.min(*s + 1)),
Self::Clip(t, s) => Self::Clip(*t, last_scene.min(*s + 1)),
}
}
pub fn scene_prev (&mut self) {
*self = match self {
Self::Mix => Self::Mix,
Self::Track(t) => Self::Track(*t),
Self::Scene(s) => if *s == 0 {
Self::Mix
} else {
Self::Scene(*s - 1)
},
Self::Clip(t, s) => if *s == 0 {
Self::Track(*t)
} else {
Self::Clip(*t, s.saturating_sub(1))
}
}
}
}

View file

@ -0,0 +1,57 @@
use crate::*;
use super::Arranger;
/// Phrase management methods
impl Arranger {
pub fn phrase (&self) -> Option<&Arc<RwLock<Phrase>>> {
let track_id = self.selected.track()?;
self.tracks.get(track_id)?.phrases.get((*self.scene()?.clips.get(track_id)?)?)
}
pub fn phrase_next (&mut self) {
let track_index = self.selected.track();
let scene_index = self.selected.scene();
track_index
.and_then(|index|self.tracks.get_mut(index).map(|track|(index, track)))
.and_then(|(track_index, track)|{
let phrases = track.phrases.len();
scene_index
.and_then(|index|self.scenes.get_mut(index))
.and_then(|scene|{
if let Some(phrase_index) = scene.clips[track_index] {
if phrase_index >= phrases - 1 {
scene.clips[track_index] = None;
} else {
scene.clips[track_index] = Some(phrase_index + 1);
}
} else if phrases > 0 {
scene.clips[track_index] = Some(0);
}
Some(())
})
});
}
pub fn phrase_prev (&mut self) {
let track_index = self.selected.track();
let scene_index = self.selected.scene();
track_index
.and_then(|index|self.tracks.get_mut(index).map(|track|(index, track)))
.and_then(|(track_index, track)|{
let phrases = track.phrases.len();
scene_index
.and_then(|index|self.scenes.get_mut(index))
.and_then(|scene|{
if let Some(phrase_index) = scene.clips[track_index] {
scene.clips[track_index] = if phrase_index == 0 {
None
} else {
Some(phrase_index - 1)
};
} else if phrases > 0 {
scene.clips[track_index] = Some(phrases - 1);
}
Some(())
})
});
}
}

View file

@ -0,0 +1,80 @@
use crate::*;
use super::Arranger;
/// A collection of phrases to play on each track.
pub struct Scene {
pub name: String,
pub clips: Vec<Option<usize>>,
}
impl Scene {
pub fn new (name: impl AsRef<str>, clips: impl AsRef<[Option<usize>]>) -> Self {
let name = name.as_ref().into();
let clips = clips.as_ref().iter().map(|x|x.clone()).collect();
Self { name, clips, }
}
/// Returns the pulse length of the longest phrase in the scene
pub fn pulses (&self, tracks: &[Track]) -> usize {
self.clips.iter().enumerate()
.filter_map(|(i, c)|c.map(|c|tracks[i].phrases.get(c)))
.filter_map(|p|p)
.fold(0, |a, p|a.max(p.read().unwrap().length))
}
/// Returns true if all phrases in the scene are currently playing
pub fn is_playing (&self, tracks: &[Track]) -> bool {
self.clips.iter().enumerate()
.all(|(track_index, phrase_index)|match phrase_index {
Some(i) => tracks[track_index].sequence == Some(*i),
None => true
})
}
}
pub fn scene_name_max_len (scenes: &[Scene]) -> usize {
scenes.iter()
.map(|s|s.name.len())
.fold(0, usize::max)
}
pub fn scene_ppqs (tracks: &[Track], scenes: &[Scene]) -> Vec<(usize, usize)> {
let mut total = 0;
let mut scenes: Vec<(usize, usize)> = scenes.iter().map(|scene|{
let pulses = scene.pulses(tracks);
total = total + pulses;
(pulses, total - pulses)
}).collect();
scenes.push((0, total));
scenes
}
/// Scene management methods
impl Arranger {
pub fn scene (&self) -> Option<&Scene> {
self.selected.scene().map(|s|self.scenes.get(s)).flatten()
}
pub fn scene_mut (&mut self) -> Option<&mut Scene> {
self.selected.scene().map(|s|self.scenes.get_mut(s)).flatten()
}
pub fn scene_next (&mut self) {
self.selected.scene_next(self.scenes.len() - 1)
}
pub fn scene_prev (&mut self) {
self.selected.scene_prev()
}
pub fn scene_add (&mut self, name: Option<&str>) -> Usually<&mut Scene> {
let clips = vec![None;self.tracks.len()];
self.scenes.push(match name {
Some(name) => Scene::new(name, clips),
None => Scene::new(&self.scene_default_name(), clips),
});
let index = self.scenes.len() - 1;
Ok(&mut self.scenes[index])
}
pub fn scene_del (&mut self) {
unimplemented!("Arranger::scene_del");
}
pub fn scene_default_name (&self) -> String {
format!("Scene {}", self.scenes.len() + 1)
}
}

View file

@ -0,0 +1,54 @@
use crate::*;
use super::Arranger;
/// Track management methods
impl Arranger {
pub fn track (&self) -> Option<&Track> {
self.selected.track().map(|t|self.tracks.get(t)).flatten()
}
pub fn track_mut (&mut self) -> Option<&mut Track> {
self.selected.track().map(|t|self.tracks.get_mut(t)).flatten()
}
pub fn track_next (&mut self) {
self.selected.track_next(self.tracks.len() - 1)
}
pub fn track_prev (&mut self) {
self.selected.track_prev()
}
pub fn track_add (&mut self, name: Option<&str>) -> Usually<&mut Track> {
self.tracks.push(name.map_or_else(
|| Track::new(&self.track_default_name()),
|name| Track::new(name),
)?);
let index = self.tracks.len() - 1;
Ok(&mut self.tracks[index])
}
pub fn track_del (&mut self) {
unimplemented!("Arranger::track_del");
}
pub fn track_default_name (&self) -> String {
format!("Track {}", self.tracks.len() + 1)
}
}
pub fn track_name_max_len (tracks: &[Track]) -> usize {
tracks.iter()
.map(|s|s.name.len())
.fold(0, usize::max)
}
pub fn track_clip_name_lengths (tracks: &[Track]) -> Vec<(usize, usize)> {
let mut total = 0;
let mut lengths: Vec<(usize, usize)> = tracks.iter().map(|track|{
let len = 2 + track.phrases
.iter()
.fold(track.name.len(), |len, phrase|{
len.max(phrase.read().unwrap().name.len())
});
total = total + len;
(len, total - len)
}).collect();
lengths.push((0, total));
lengths
}

View file

@ -0,0 +1,200 @@
use crate::*;
use super::{Arranger, arr_track::*};
pub fn draw (state: &Arranger, buf: &mut Buffer, mut area: Rect) -> Usually<Rect> {
area.height = area.height.min((2 + state.tracks.len() * 2) as u16);
Layered([
&to_fill_bg(Nord::bg_lo(state.focused, state.entered)),
&Split::right([
&track_name_column(state),
&track_mon_column(state),
&track_rec_column(state),
&track_ovr_column(state),
&track_del_column(state),
&track_gain_column(state),
&track_scenes_column(state),
]),
]).render(buf, area)
}
fn track_name_column <'a> (state: &'a Arranger) -> impl Render + 'a {
let dim = Some(Style::default().dim());
let yellow = Some(Style::default().yellow().bold().not_dim());
let white = Some(Style::default().white().bold().not_dim());
move |buf: &mut Buffer, mut area: Rect| {
area.width = 3 + 5.max(track_name_max_len(state.tracks.as_slice())) as u16;
let offset = 0; // track scroll offset
for y in 0..area.height {
if y == 0 {
"Mixer".blit(buf, area.x + 1, area.y + y, dim)?;
} else if y % 2 == 0 {
let index = (y as usize - 2) / 2 + offset;
if let Some(track) = state.tracks.get(index) {
let selected = state.selected.track() == Some(index);
let style = if selected { yellow } else { white };
format!(" {index:>02} ").blit(buf, area.x, area.y + y, style)?;
track.name.blit(buf, area.x + 4, area.y + y, style)?;
}
}
}
Ok(area)
}
}
fn track_mon_column <'a> (state: &'a Arranger) -> impl Render + 'a {
let on = Some(Style::default().not_dim().green().bold());
let off = Some(Style::default().dim());
move |buf: &mut Buffer, mut area: Rect| {
area.x = area.x + 1;
for y in 0..area.height {
if y == 0 {
//" MON ".blit(buf, area.x, area.y + y, style2)?;
} else if y % 2 == 0 {
let index = (y as usize - 2) / 2;
if let Some(track) = state.tracks.get(index) {
let style = if track.monitoring { on } else { off };
" MON ".blit(buf, area.x, area.y + y, style)?;
} else {
area.height = y;
break
}
}
}
area.width = 4;
Ok(area)
}
}
fn track_rec_column <'a> (state: &'a Arranger) -> impl Render + 'a {
let on = Some(Style::default().not_dim().red().bold());
let off = Some(Style::default().dim());
move |buf: &mut Buffer, mut area: Rect| {
area.x = area.x + 1;
for y in 0..area.height {
if y == 0 {
//" REC ".blit(buf, area.x, area.y + y, style2)?;
} else if y % 2 == 0 {
let index = (y as usize - 2) / 2;
if let Some(track) = state.tracks.get(index) {
let style = if track.recording { on } else { off };
" REC ".blit(buf, area.x, area.y + y, style)?;
} else {
area.height = y;
break
}
}
}
area.width = 4;
Ok(area)
}
}
fn track_ovr_column <'a> (state: &'a Arranger) -> impl Render + 'a {
let on = Some(Style::default().not_dim().yellow().bold());
let off = Some(Style::default().dim());
move |buf: &mut Buffer, mut area: Rect| {
area.x = area.x + 1;
for y in 0..area.height {
if y == 0 {
//" OVR ".blit(buf, area.x, area.y + y, style2)?;
} else if y % 2 == 0 {
let index = (y as usize - 2) / 2;
if let Some(track) = state.tracks.get(index) {
" OVR ".blit(buf, area.x, area.y + y, if track.overdub {
on
} else {
off
})?;
} else {
area.height = y;
break
}
}
}
area.width = 4;
Ok(area)
}
}
fn track_del_column <'a> (state: &'a Arranger) -> impl Render + 'a {
let off = Some(Style::default().dim());
move |buf: &mut Buffer, mut area: Rect| {
area.x = area.x + 1;
for y in 0..area.height {
if y == 0 {
//" DEL ".blit(buf, area.x, area.y + y, style2)?;
} else if y % 2 == 0 {
let index = (y as usize - 2) / 2;
if let Some(_) = state.tracks.get(index) {
" DEL ".blit(buf, area.x, area.y + y, off)?;
} else {
area.height = y;
break
}
}
}
area.width = 4;
Ok(area)
}
}
fn track_gain_column <'a> (state: &'a Arranger) -> impl Render + 'a {
let off = Some(Style::default().dim());
move |buf: &mut Buffer, mut area: Rect| {
area.x = area.x + 1;
for y in 0..area.height {
if y == 0 {
//" GAIN ".blit(buf, area.x, area.y + y, style2)?;
} else if y % 2 == 0 {
let index = (y as usize - 2) / 2;
if let Some(_) = state.tracks.get(index) {
" +0.0 ".blit(buf, area.x, area.y + y, off)?;
} else {
area.height = y;
break
}
}
}
area.width = 7;
Ok(area)
}
}
fn track_scenes_column <'a> (state: &'a Arranger) -> impl Render + 'a {
|buf: &mut Buffer, area: Rect| {
let mut x2 = 0;
let Rect { x, y, height, .. } = area;
for (scene_index, scene) in state.scenes.iter().enumerate() {
let active_scene = state.selected.scene() == Some(scene_index);
let sep = Some(if active_scene {
Style::default().yellow().not_dim()
} else {
Style::default().dim()
});
for y in y+1..y+height {
"".blit(buf, x + x2, y, sep)?;
}
let mut x3 = scene.name.len() as u16;
scene.name.blit(buf, x + x2, y, sep)?;
for (i, clip) in scene.clips.iter().enumerate() {
let active_track = state.selected.track() == Some(i);
if let Some(clip) = clip {
let y2 = y + 2 + i as u16 * 2;
let label = match state.tracks[i].phrases.get(*clip) {
Some(phrase) => &format!("{}", phrase.read().unwrap().name),
None => "...."
};
label.blit(buf, x + x2, y2, Some(if active_track && active_scene {
Style::default().not_dim().yellow().bold()
} else {
Style::default().not_dim()
}))?;
x3 = x3.max(label.len() as u16)
}
}
x2 = x2 + x3 + 1;
}
Ok(Rect { x, y, height, width: x2 })
}
}

View file

@ -0,0 +1,225 @@
use crate::*;
use super::{
Arranger,
arr_focus::ArrangerFocus,
arr_track::track_clip_name_lengths,
arr_scene::{Scene, scene_ppqs, scene_name_max_len}
};
pub fn draw_expanded (state: &Arranger, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
let track_cols = track_clip_name_lengths(state.tracks.as_slice());
let scene_rows = scene_ppqs(state.tracks.as_slice(), state.scenes.as_slice());
draw(state, buf, area, track_cols.as_slice(), scene_rows.as_slice())
}
pub fn draw_compact (state: &Arranger, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
let track_cols = track_clip_name_lengths(state.tracks.as_slice());
let scene_rows = (0..=state.scenes.len()+3).map(|i|(96, 96*i)).collect::<Vec<_>>();
draw(state, buf, area, track_cols.as_slice(), scene_rows.as_slice())
}
pub fn draw (
state: &Arranger,
buf: &mut Buffer,
mut area: Rect,
cols: &[(usize, usize)],
rows: &[(usize, usize)],
) -> Usually<Rect> {
area.height = 2 + (rows[rows.len() - 1].1 / 96) as u16;
let offset = 3 + scene_name_max_len(state.scenes.as_ref()) as u16;
Layered([
&to_fill_bg(Nord::bg_lo(state.focused, state.entered)),
&column_separators(offset, cols),
&cursor_focus(state, offset, cols, rows),
&Split::down([
&tracks_header(state, cols, offset),
&scene_rows(state, cols, rows, offset),
]),
&row_separators(rows),
]).render(buf, area)
}
fn column_separators <'a> (offset: u16, cols: &'a [(usize, usize)]) -> impl Render + 'a {
move |buf: &mut Buffer, area: Rect|{
let style = Some(Style::default().fg(Nord::SEPARATOR));
for (_, x) in cols.iter() {
let x = offset + area.x + *x as u16 - 1;
for y in area.y..area.height+area.y {
"".blit(buf, x, y, style)?;
}
}
Ok(area)
}
}
fn row_separators <'a> (rows: &'a [(usize, usize)]) -> impl Render + 'a {
move |buf: &mut Buffer, area: Rect| {
for (_, y) in rows.iter() {
let y = area.y + (*y / 96) as u16 + 1;
if y >= buf.area.height {
break
}
for x in area.x..area.width+area.y-2 {
let cell = buf.get_mut(x, y);
cell.modifier = Modifier::UNDERLINED;
cell.underline_color = Nord::SEPARATOR;
}
}
Ok(area)
}
}
fn cursor_focus <'a> (
state: &'a Arranger, offset: u16, cols: &'a [(usize, usize)], rows: &'a [(usize, usize)],
) -> impl Render + 'a {
move |buf: &mut Buffer, area: Rect| {
match state.selected {
ArrangerFocus::Mix => if state.focused
&& state.entered
&& state.selected == ArrangerFocus::Mix
{
fill_bg(buf, area, Nord::bg_hi(state.focused, state.entered));
Corners(Style::default().green().not_dim()).draw(buf, area)
} else {
Ok(area)
},
ArrangerFocus::Track(t) => {
let area = Rect {
x: offset + area.x + cols[t].1 as u16 - 1,
y: area.y,
width: cols[t].0 as u16,
height: area.height
};
fill_bg(buf, area, Nord::bg_hi(state.focused, state.entered));
Corners(Style::default().green().not_dim()).draw(buf, area)
},
ArrangerFocus::Scene(s) => {
let area = Rect {
x: area.x,
y: 2 + area.y + (rows[s].1 / 96) as u16,
width: area.width,
height: (rows[s].0 / 96) as u16
};
fill_bg(buf, area, Nord::bg_hi(state.focused, state.entered));
Corners(Style::default().green().not_dim()).draw(buf, area)
},
ArrangerFocus::Clip(t, s) => {
let track_area = Rect {
x: offset + area.x + cols[t].1 as u16 - 1,
y: area.y,
width: cols[t].0 as u16,
height: area.height
};
let scene_area = Rect {
x: area.x,
y: 2 + area.y + (rows[s].1 / 96) as u16,
width: area.width,
height: (rows[s].0 / 96) as u16
};
let area = Rect {
x: offset + area.x + cols[t].1 as u16 - 1,
y: 2 + area.y + (rows[s].1 / 96) as u16,
width: cols[t].0 as u16,
height: (rows[s].0 / 96) as u16
};
let lo = Nord::bg_hi(state.focused, state.entered);
let hi = Nord::bg_hier(state.focused, state.entered);
fill_bg(buf, track_area, lo);
fill_bg(buf, scene_area, lo);
fill_bg(buf, area, hi);
Corners(Style::default().green().not_dim()).draw(buf, area)
},
}
}
}
pub fn tracks_header <'a> (
state: &'a Arranger,
track_cols: &'a [(usize, usize)],
offset: u16,
) -> impl Render + 'a {
move |buf: &mut Buffer, area: Rect| {
let Rect { y, width, .. } = area;
for (track, (_, x)) in state.tracks.iter().zip(track_cols) {
let x = *x as u16;
if x > width {
break
}
track.name.blit(buf, offset + x, y, Some(Style::default()))?;
}
Ok(Rect { x: area.x, y, width, height: 2 })
}
}
pub fn scene_rows <'a> (
state: &'a Arranger,
track_cols: &'a [(usize, usize)],
scene_rows: &'a [(usize, usize)],
offset: u16,
) -> impl Render + 'a {
move |buf: &mut Buffer, area: Rect| {
let black = Some(Style::default().fg(Nord::SEPARATOR));
let Rect { mut y, height, .. } = area;
for (_, x) in track_cols.iter() {
let x = *x as u16;
if x > 0 {
for y in area.y-2..y-2 {
"".blit(buf, x - 1, y, black)?;
}
}
}
for (scene, (pulses, _)) in state.scenes.iter().zip(scene_rows) {
if y > height {
break
}
let h = 1.max((pulses / 96) as u16);
scene_row(state, buf, Rect {
x: area.x,
y,
width: area.width,
height: h,//.min(area.height - y)
}, scene, track_cols, offset)?;
y = y + h
}
Ok(area)
}
}
fn scene_row (
state: &Arranger,
buf: &mut Buffer,
area: Rect,
scene: &Scene,
track_cols: &[(usize, usize)],
offset: u16
) -> Usually<u16> {
let Rect { y, width, .. } = area;
let tracks = state.tracks.as_ref();
let playing = scene.is_playing(tracks);
(if playing { "" } else { " " }).blit(buf, area.x, y, None)?;
scene.name.blit(buf, area.x + 1, y, None)?;
let style = Some(Style::default().white());
for (track, (w, x)) in track_cols.iter().enumerate() {
let x = *x as u16 + offset;
if x > width {
break
}
if let (Some(track), Some(Some(clip))) = (
tracks.get(track), scene.clips.get(track)
) {
if let Some(phrase) = track.phrases.get(*clip) {
let phrase = phrase.read().unwrap();
phrase.name.blit(buf, x, y, style)?;
if track.sequence == Some(*clip) {
fill_bg(buf, Rect {
x: x - 1,
y,
width: *w as u16,
height: area.height,
}, Nord::PLAYING);
}
}
}
}
Ok((scene.pulses(tracks) / 96) as u16)
}

View file

@ -0,0 +1,3 @@
fn main () {
panic!()
}

View file

@ -0,0 +1,53 @@
//! Phrase editor.
pub(crate) use tek_core::*;
pub(crate) use tek_core::ratatui::prelude::*;
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
pub(crate) use tek_core::midly::{num::u7, live::LiveEvent, MidiMessage};
pub(crate) use tek_jack::{*, jack::*};
pub(crate) use tek_timer::*;
pub(crate) use std::sync::{Arc, RwLock};
submod! { midi phrase arranger sequencer sequencer_track }
/// Key bindings for phrase editor.
pub const KEYMAP_SEQUENCER: &'static [KeyBinding<Sequencer>] = keymap!(Sequencer {
[Up, NONE, "seq_cursor_up", "move cursor up", |sequencer: &mut Sequencer| {
match sequencer.entered {
true => { sequencer.note_axis.point_dec(); },
false => { sequencer.note_axis.start_dec(); },
}
Ok(true)
}],
[Down, NONE, "seq_cursor_down", "move cursor down", |sequencer: &mut Sequencer| {
match sequencer.entered {
true => { sequencer.note_axis.point_inc(); },
false => { sequencer.note_axis.start_inc(); },
}
Ok(true)
}],
[Left, NONE, "seq_cursor_left", "move cursor up", |sequencer: &mut Sequencer| {
match sequencer.entered {
true => { sequencer.time_axis.point_dec(); },
false => { sequencer.time_axis.start_dec(); },
}
Ok(true)
}],
[Right, NONE, "seq_cursor_right", "move cursor up", |sequencer: &mut Sequencer| {
match sequencer.entered {
true => { sequencer.time_axis.point_inc(); },
false => { sequencer.time_axis.start_inc(); },
}
Ok(true)
}],
[Char('`'), NONE, "seq_mode_switch", "switch the display mode", |sequencer: &mut Sequencer| {
sequencer.mode = !sequencer.mode;
Ok(true)
}],
/*
[Char('a'), NONE, "note_add", "Add note", note_add],
[Char('z'), NONE, "note_del", "Delete note", note_del],
[CapsLock, NONE, "advance", "Toggle auto advance", nop],
[Char('w'), NONE, "rest", "Advance by note duration", nop],
*/
});

View file

@ -0,0 +1,36 @@
use crate::*;
use tek_jack::jack::*;
/// MIDI message serialized to bytes
pub type MIDIMessage = Vec<u8>;
/// Collection of serialized MIDI messages
pub type MIDIChunk = [Vec<MIDIMessage>];
/// Add "all notes off" to the start of a buffer.
pub fn all_notes_off (output: &mut MIDIChunk) {
let mut buf = vec![];
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
evt.write(&mut buf).unwrap();
output[0].push(buf);
}
/// Return boxed iterator of MIDI events
pub fn parse_midi_input (input: MidiIter) -> Box<dyn Iterator<Item=(usize, LiveEvent, &[u8])> + '_> {
Box::new(input.map(|RawMidi { time, bytes }|(
time as usize,
LiveEvent::parse(bytes).unwrap(),
bytes
)))
}
/// Write to JACK port from output buffer (containing notes from sequence and/or monitor)
pub fn write_midi_output (writer: &mut MidiWriter, output: &MIDIChunk, frames: usize) {
for time in 0..frames {
for event in output[time].iter() {
writer.write(&RawMidi { time: time as u32, bytes: &event })
.expect(&format!("{event:?}"));
}
}
}

View file

@ -0,0 +1,93 @@
use crate::*;
/// Define a MIDI phrase.
#[macro_export] macro_rules! phrase {
($($t:expr => $msg:expr),* $(,)?) => {{
#[allow(unused_mut)]
let mut phrase = BTreeMap::new();
$(phrase.insert($t, vec![]);)*
$(phrase.get_mut(&$t).unwrap().push($msg);)*
phrase
}}
}
pub type PhraseData = Vec<Vec<MidiMessage>>;
#[derive(Debug)]
/// A MIDI sequence.
pub struct Phrase {
pub name: String,
pub length: usize,
pub notes: PhraseData,
pub looped: Option<(usize, usize)>,
/// All notes are displayed with minimum length
pub percussive: bool
}
impl Default for Phrase {
fn default () -> Self {
Self::new("", 0, None)
}
}
impl Phrase {
pub fn new (name: &str, length: usize, notes: Option<PhraseData>) -> Self {
Self {
name: name.to_string(),
length,
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
looped: Some((0, length)),
percussive: true,
}
}
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
if pulse >= self.length {
panic!("extend phrase first")
}
self.notes[pulse].push(message);
}
/// Check if a range `start..end` contains MIDI Note On `k`
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
//panic!("{:?} {start} {end}", &self);
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
for event in events.iter() {
match event {
MidiMessage::NoteOn {key,..} => {
if *key == k {
return true
}
}
_ => {}
}
}
}
return false
}
/// Write a chunk of MIDI events to an output port.
pub fn process_out (
&self,
output: &mut MIDIChunk,
notes_on: &mut [bool;128],
timebase: &Arc<Timebase>,
(frame0, frames, _): (usize, usize, f64),
) {
let mut buf = Vec::with_capacity(8);
for (time, tick) in Ticks(timebase.pulse_per_frame()).between_frames(
frame0, frame0 + frames
) {
let tick = tick % self.length;
for message in self.notes[tick].iter() {
buf.clear();
let channel = 0.into();
let message = *message;
LiveEvent::Midi { channel, message }.write(&mut buf).unwrap();
output[time as usize].push(buf.clone());
match message {
MidiMessage::NoteOn { key, .. } => notes_on[key.as_int() as usize] = true,
MidiMessage::NoteOff { key, .. } => notes_on[key.as_int() as usize] = false,
_ => {}
}
}
}
}
}

View file

@ -0,0 +1,332 @@
use crate::*;
/// Phrase editor.
pub struct Sequencer {
pub mode: bool,
pub focused: bool,
pub entered: bool,
pub phrase: Option<Arc<RwLock<Phrase>>>,
pub buffer: BigBuffer,
pub keys: Buffer,
/// Highlight input keys
pub keys_in: [bool; 128],
/// Highlight output keys
pub keys_out: [bool; 128],
pub now: usize,
pub ppq: usize,
pub note_axis: FixedAxis<usize>,
pub time_axis: ScaledAxis<usize>,
}
render!(Sequencer |self, buf, area| {
fill_bg(buf, area, Nord::bg_lo(self.focused, self.entered));
self.horizontal_draw(buf, area)?;
if self.focused && self.entered {
Corners(Style::default().green().not_dim()).draw(buf, area)?;
}
Ok(area)
});
impl Sequencer {
pub fn new () -> Self {
Self {
buffer: Default::default(),
keys: keys_vert(),
entered: false,
focused: false,
mode: false,
keys_in: [false;128],
keys_out: [false;128],
phrase: None,
now: 0,
ppq: 96,
note_axis: FixedAxis {
start: 12,
point: Some(36)
},
time_axis: ScaledAxis {
start: 0,
scale: 24,
point: Some(0)
},
}
}
/// 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 = usize::MAX.min(phrase.read().unwrap().length);
let mut buffer = BigBuffer::new(width, 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 = Default::default();
}
Ok(())
}
fn style_focus (&self) -> Option<Style> {
Some(if self.focused {
Style::default().green().not_dim()
} else {
Style::default().green().dim()
})
}
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()
}
}
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.keys_in[index] {
Color::Red
} else if self.keys_out[index] {
Color::Green
} else {
default
}
}
const H_KEYS_OFFSET: usize = 5;
fn horizontal_draw (&self, buf: &mut Buffer, area: Rect) -> Usually<()> {
self.horizontal_keys(buf, area)?;
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> {
if area.height < 2 {
return Ok(area)
}
let area = Rect {
x: area.x + Self::H_KEYS_OFFSET as u16,
y: area.y + 1,
width: area.width - Self::H_KEYS_OFFSET as u16,
height: area.height - 2
};
buffer_update(buf, area, &move |cell, x, y|{
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);
});
}
});
Ok(area)
}
fn horizontal_keys (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
if area.height < 2 {
return Ok(area)
}
let area = Rect {
x: area.x,
y: area.y + 1,
width: 5,
height: area.height - 2
};
buffer_update(buf, area, &|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()
}
});
Ok(area)
}
fn horizontal_quant (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
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> {
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 { "" };
c.blit(buf, x, y, self.style_focus())
} else {
Ok(Rect::default())
}
}
fn horizontal_timer (
&self, buf: &mut Buffer, area: Rect, phrase: &RwLock<Phrase>
) -> Usually<Rect> {
let phrase = phrase.read().unwrap();
let (time0, time_z, now) = (self.time_axis.start, self.time_axis.scale, self.now % phrase.length);
let Rect { x, width, .. } = 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 = (time0 + x2) * time_z;
let next_step = (time0 + x2 + 1) * time_z;
let style = Self::style_timer_step(now, step as usize, next_step as usize);
"-".blit(buf, x as u16, area.y, Some(style))?;
}
return Ok(Rect { x: area.x, y: area.y, width: area.width, height: 1 })
}
}
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| {
let y = 63 - y;
match x {
0 => {
cell.set_char('▀');
let (fg, bg) = key_colors(6 - y % 6);
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));
},
_ => {}
}
});
buffer
}
fn nth_octave (index: u16) -> &'static str {
match index {
0 => "-1",
1 => "0",
2 => "1",
3 => "2",
4 => "3",
5 => "4",
6 => "5",
7 => "6",
8 => "7",
9 => "8",
10 => "9",
_ => unreachable!()
}
}
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 BigBuffer, length: usize, ppq: usize) -> Usually<()> {
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(char_seq_bg(ppq, x as u16));
cell.set_fg(Color::Gray);
cell.modifier = Modifier::DIM;
});
}
}
Ok(())
}
fn char_seq_bg (ppq: usize, x: u16) -> char {
if ppq == 0 {
'·'
} else if x % (4 * ppq as u16) == 0 {
'│'
} else if x % ppq as u16 == 0 {
'╎'
} else {
'·'
}
}
fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) -> Usually<()> {
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);
}
}
}
Ok(())
}

View file

@ -0,0 +1,227 @@
use crate::*;
use tek_core::Direction;
/// A sequencer track.
#[derive(Debug)]
pub struct SequencerTrack {
pub name: String,
/// Play input through output.
pub monitoring: bool,
/// Write input to sequence.
pub recording: bool,
/// Overdub input to sequence.
pub overdub: bool,
/// Map: tick -> MIDI events at tick
pub phrases: Vec<Arc<RwLock<Phrase>>>,
/// Phrase selector
pub sequence: Option<usize>,
/// Output from current sequence.
pub midi_out: Option<Port<MidiOut>>,
/// MIDI output buffer
midi_out_buf: Vec<Vec<Vec<u8>>>,
/// Send all notes off
pub reset: bool, // TODO?: after Some(nframes)
/// Highlight keys on piano roll.
pub notes_in: [bool;128],
/// Highlight keys on piano roll.
pub notes_out: [bool;128],
}
impl SequencerTrack {
pub fn new (name: &str) -> Usually<Self> {
Ok(Self {
name: name.to_string(),
monitoring: false,
recording: false,
overdub: true,
phrases: vec![],
sequence: None,
midi_out: None,
midi_out_buf: vec![Vec::with_capacity(16);16384],
reset: true,
notes_in: [false;128],
notes_out: [false;128],
})
}
pub fn toggle_monitor (&mut self) {
self.monitoring = !self.monitoring;
}
pub fn toggle_record (&mut self) {
self.recording = !self.recording;
}
pub fn toggle_overdub (&mut self) {
self.overdub = !self.overdub;
}
pub fn process (
&mut self,
input: Option<MidiIter>,
timebase: &Arc<Timebase>,
playing: Option<TransportState>,
started: Option<(usize, usize)>,
quant: usize,
reset: bool,
scope: &ProcessScope,
(frame0, frames): (usize, usize),
(_usec0, _usecs): (usize, usize),
period: f64,
) {
if self.midi_out.is_some() {
// Clear the section of the output buffer that we will be using
for frame in &mut self.midi_out_buf[0..frames] {
frame.clear();
}
// Emit "all notes off" at start of buffer if requested
if self.reset {
all_notes_off(&mut self.midi_out_buf);
self.reset = false;
} else if reset {
all_notes_off(&mut self.midi_out_buf);
}
}
if let (
Some(TransportState::Rolling), Some((start_frame, _)), Some(phrase)
) = (
playing, started, self.sequence.and_then(|id|self.phrases.get_mut(id))
) {
phrase.read().map(|phrase|{
if self.midi_out.is_some() {
phrase.process_out(
&mut self.midi_out_buf,
&mut self.notes_out,
timebase,
(frame0.saturating_sub(start_frame), frames, period)
);
}
}).unwrap();
let mut phrase = phrase.write().unwrap();
let length = phrase.length;
// Monitor and record input
if input.is_some() && (self.recording || self.monitoring) {
// For highlighting keys and note repeat
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
match event {
LiveEvent::Midi { message, .. } => {
if self.monitoring {
self.midi_out_buf[frame].push(bytes.to_vec())
}
if self.recording {
phrase.record_event({
let pulse = timebase.frame_to_pulse(
(frame0 + frame - start_frame) as f64
);
let quantized = (
pulse / quant as f64
).round() as usize * quant;
let looped = quantized % length;
looped
}, message);
}
match message {
MidiMessage::NoteOn { key, .. } => {
self.notes_in[key.as_int() as usize] = true;
}
MidiMessage::NoteOff { key, .. } => {
self.notes_in[key.as_int() as usize] = false;
},
_ => {}
}
},
_ => {}
}
}
}
} else if input.is_some() && self.midi_out.is_some() && self.monitoring {
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
self.process_monitor_event(frame, &event, bytes)
}
}
if let Some(out) = &mut self.midi_out {
write_midi_output(&mut out.writer(scope), &self.midi_out_buf, frames);
}
}
#[inline]
fn process_monitor_event (&mut self, frame: usize, event: &LiveEvent, bytes: &[u8]) {
match event {
LiveEvent::Midi { message, .. } => {
self.write_to_output_buffer(frame, bytes);
self.process_monitor_message(&message);
},
_ => {}
}
}
#[inline] fn write_to_output_buffer (&mut self, frame: usize, bytes: &[u8]) {
self.midi_out_buf[frame].push(bytes.to_vec());
}
#[inline]
fn process_monitor_message (&mut self, message: &MidiMessage) {
match message {
MidiMessage::NoteOn { key, .. } => {
self.notes_in[key.as_int() as usize] = true;
}
MidiMessage::NoteOff { key, .. } => {
self.notes_in[key.as_int() as usize] = false;
},
_ => {}
}
}
}
pub struct ChainView<'a> {
pub track: Option<&'a Track>,
pub direction: Direction,
pub focused: bool,
pub entered: bool,
}
impl<'a> ChainView<'a> {
pub fn horizontal (app: &'a App) -> Self {
Self::new(app, Direction::Right)
}
pub fn vertical (app: &'a App) -> Self {
Self::new(app, Direction::Down)
}
pub fn new (app: &'a App, direction: Direction) -> Self {
Self {
direction,
entered: app.entered,
focused: app.section == AppFocus::Chain,
track: app.arranger.track()
}
}
}
impl<'a> Render for ChainView<'a> {
fn render (&self, buf: &mut Buffer, mut area: Rect) -> Usually<Rect> {
if let Some(track) = self.track {
match self.direction {
Direction::Down => area.width = area.width.min(40),
Direction::Right => area.width = area.width.min(10),
}
fill_bg(buf, area, Nord::bg_lo(self.focused, self.entered));
let (area, areas) = self.direction
.split_focus(0, track.devices.as_slice(), if self.focused {
Style::default().green().dim()
} else {
Style::default().dim()
})
.render_areas(buf, area)?;
if self.focused && self.entered {
Corners(Style::default().green().not_dim()).draw(buf, areas[0])?;
}
Ok(area)
} else {
let Rect { x, y, width, height } = area;
let label = "No track selected";
let x = x + (width - label.len() as u16) / 2;
let y = y + height / 2;
label.blit(buf, x, y, Some(Style::default().dim().bold()))?;
Ok(area)
}
}
}