diff --git a/Cargo.lock b/Cargo.lock index 87cbb7e3..b4a9c676 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1044,7 +1044,7 @@ dependencies = [ [[package]] name = "tek" -version = "0.0.0" +version = "0.1.0" dependencies = [ "atomic_float", "backtrace", diff --git a/Cargo.toml b/Cargo.toml index e4cd0e14..bd327bae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "tek" edition = "2021" +version = "0.1.0" [dependencies] jack = "0.10" clap = { version = "4.5.4", features = [ "derive" ] } crossterm = "0.27" -ratatui = { version = "0.26.3", features = [ "unstable-widget-ref" ] } +ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] } backtrace = "0.3.72" microxdg = "0.1.2" toml = "0.8.12" diff --git a/demos/project.edn b/demos/project.edn index badf0102..288d577d 100644 --- a/demos/project.edn +++ b/demos/project.edn @@ -55,7 +55,7 @@ (:12 (44 100) (40 100)) (:14 (44 100))) - (phrase { :name "Trapping" :beats 8 :steps 96 } + (phrase { :name "Trap Pinging" :beats 8 :steps 96 } (:00 (42 100) (36 100) (34 120) (49 100)) (:01 (42 100)) (:02 (42 100)) diff --git a/shell.nix b/shell.nix old mode 100644 new mode 100755 index c811f617..bef81b01 --- a/shell.nix +++ b/shell.nix @@ -1,3 +1,4 @@ +#!/usr/bin/env nix-shell {pkgs?import{}}:pkgs.mkShell{ nativeBuildInputs = with pkgs; [ pkg-config diff --git a/src/core/render.rs b/src/core/render.rs index 027579b9..b3db7454 100644 --- a/src/core/render.rs +++ b/src/core/render.rs @@ -6,13 +6,15 @@ 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) + buf: &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); + if x < buf.area.width && y < buf.area.height { + callback(buf.get_mut(x, y), col, row); + } } } } @@ -63,7 +65,7 @@ pub trait Render: Send { ($T:ty) => { impl Render for $T {} }; - ($T:ty |$self:ident, $buf:ident, $area:ident|$block:tt) => { + ($T:ty |$self:ident, $buf:ident, $area:ident|$block:expr) => { impl Render for $T { fn render (&$self, $buf: &mut Buffer, $area: Rect) -> Usually { $block diff --git a/src/devices/arranger.rs b/src/devices/arranger.rs index 97528e7c..04b960cf 100644 --- a/src/devices/arranger.rs +++ b/src/devices/arranger.rs @@ -1,6 +1,8 @@ //! Clip launcher and arrangement editor. -use crate::{core::*, model::*, view::*}; +use crate::{core::*, model::*}; +use self::focus::ArrangerFocus; +pub use self::scene::Scene; /// Key bindings for arranger section. pub const KEYMAP_ARRANGER: &'static [KeyBinding] = keymap!(App { @@ -59,48 +61,30 @@ pub const KEYMAP_ARRANGER: &'static [KeyBinding] = keymap!(App { /// Represents the tracks and scenes of the composition. pub struct Arranger { /// Display mode of arranger - pub mode: bool, + pub mode: bool, /// Currently selected element. pub selected: ArrangerFocus, /// Collection of tracks. - pub tracks: Vec, + pub tracks: Vec, /// Collection of scenes. - pub scenes: Vec, + pub scenes: Vec, pub focused: bool, pub entered: bool, + pub fixed_height: bool, } -render!(Arranger |self, buf, area| { - let mut area = area; - area.height = area.height.min(1 + if self.mode { - self.tracks.len() as u16 * 2 - } else { - self.scenes.len() as u16 * 2 - }); - fill_bg(buf, area, Nord::bg_lo(self.focused, self.entered)); - area = if self.mode { - self.draw_horizontal(buf, area)? - } else { - let area = self.draw_vertical(buf, area)?; - if self.focused && self.entered && self.selected == ArrangerFocus::Mix { - Corners(Style::default().green().not_dim()).draw(buf, area)?; - }; - area - }; - Ok(area) -}); - impl Arranger { pub fn new () -> Self { Self { - mode: false, - selected: ArrangerFocus::Clip(0, 0), - scenes: vec![], - tracks: vec![], - entered: true, - focused: true, + mode: false, + selected: ArrangerFocus::Clip(0, 0), + scenes: vec![], + tracks: vec![], + entered: true, + focused: true, + fixed_height: false, } } @@ -119,482 +103,683 @@ impl Arranger { _ => {} } } +} - fn draw_vertical (&self, buf: &mut Buffer, area: Rect) -> Usually { - let bg_hi = Nord::bg_hier(self.focused, self.entered); - let bg_lo = Nord::bg_hi(self.focused, self.entered); +render!(Arranger |self, buf, area| if self.mode { + self::draw_horizontal::draw(self, buf, area) +} else { + self::draw_vertical::draw(self, buf, area) +}); - return Split::right([ +mod draw_vertical { + use crate::{core::*, view::*, model::Track}; + use super::{Arranger, focus::ArrangerFocus, track::track_clip_name_lengths, scene::{Scene, scene_ppqs, scene_name_max_len}}; - // Scene column - &|buf: &mut Buffer, area: Rect|{ - let Rect { x, y, width, height } = area; - for (scene_index, scene) in self.scenes.iter().enumerate() { - if y + 2 * scene_index as u16 >= height { - break - } - let active = self.selected == ArrangerFocus::Scene(scene_index); - let style = Some(Nord::style_hi(self.focused, active).bold()); - let y = 1 + y + 2 * scene_index as u16; - let playing = scene.clips.iter().enumerate() - .all(|(track_index, phrase_index)|match phrase_index { - Some(i) => self.tracks[track_index].sequence == Some(*i), - None => true - }); - if playing { "" } else { " " }.blit(buf, x+1, y, style)?; - scene.name.blit(buf, x + 2, y, style)?; - if self.selected.scene() == Some(scene_index) { - let selected = self.selected == ArrangerFocus::Scene(scene_index); - let area = Rect { x, y, width, height: 2 }; - if selected { - fill_bg(buf, area, bg_hi); - if self.focused && self.entered { - Corners(Style::default().green().not_dim()).draw(buf, area)?; - } - } else { - fill_bg(buf, area, bg_lo); - } - } - } - let width = 2 + self.scenes.iter() - .map(|x|&x.name).fold(0, |x,y|x.max(y.len() as u16+1)); - Ok(Rect { width, ..area }) - }, - - // Track columns - &|buf: &mut Buffer, area: Rect|{ - let Rect { mut x, y, width, height } = area; - for (track_index, track) in self.tracks.iter().enumerate() { - if x >= width { - break - } - let width = 16u16; - track.name.blit(buf, x + 1, y, Some(Style::default().bold()))?; - for (scene_index, scene) in self.scenes.iter().enumerate() { - if y + 2 * scene_index as u16 >= height { - break - } - let label = match scene.clips.get(track_index) { - Some(Some(clip)) => if let Some(phrase) = track.phrases.get(*clip) { - let icon = if track.sequence == Some(*clip) { "" } else { "┊" }; - format!("{icon} {}", phrase.read().unwrap().name) - } else { - format!(" ??? ") - }, - _ => format!("┊ ········") - }; - let hi = self.selected == ArrangerFocus::Clip(track_index, scene_index); - let style = Some(Nord::style_hi(self.focused, hi)); - let y = 1 + y + 2 * scene_index as u16; - "┊".blit(buf, x, y + 1, style)?; - label.blit(buf, x, y, style)?; - if self.selected == ArrangerFocus::Clip(track_index, scene_index) { - fill_bg(buf, Rect { x: area.x, y, width: area.width, height: 2 }, bg_lo); - fill_bg(buf, Rect { x, y: area.y, width, height: area.height }, bg_lo); - let area = Rect { x, y, width, height: 2 }; - fill_bg(buf, area, bg_hi); - if self.focused && self.entered { - Corners(Style::default().green().not_dim()).draw(buf, area)?; - }; - } - } - if self.selected == ArrangerFocus::Track(track_index) { - fill_bg(buf, Rect { x, y: area.y, width, height: 1 }, bg_hi); - if self.focused && self.entered { - Corners(Style::default().green().not_dim()) - .draw(buf, Rect { x, y: area.y, width, height })?; - }; - } - x = x + width as u16; - } - Ok(Rect { x: area.x, y, width: x - area.x, height }) + pub fn draw (state: &Arranger, buf: &mut Buffer, mut area: Rect) -> Usually { + let track_cols = track_clip_name_lengths(state.tracks.as_slice()); + let scene_rows = scene_ppqs(state.tracks.as_slice(), state.scenes.as_slice()); + area.height = 2 + (scene_rows[scene_rows.len() - 1].1 / 96) as u16; + let offset = 3 + scene_name_max_len(state.scenes.as_ref()) as u16; + let style = Some(Style::default().fg(Color::Rgb(0,0,0))); + for (_, x) in track_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)?; } - ]).render(buf, area); + } + + fill_bg(buf, area, Nord::bg_lo(state.focused, state.entered)); + 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)?; + }, + ArrangerFocus::Track(t) => { + let area = Rect { + x: offset + area.x + track_cols[t].1 as u16 - 1, + y: area.y, + width: track_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 + (scene_rows[s].1 / 96) as u16, + width: area.width, + height: (scene_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 + track_cols[t].1 as u16 - 1, + y: area.y, + width: track_cols[t].0 as u16, + height: area.height + }; + let scene_area = Rect { + x: area.x, + y: 2 + area.y + (scene_rows[s].1 / 96) as u16, + width: area.width, + height: (scene_rows[s].0 / 96) as u16 + }; + let area = Rect { + x: offset + area.x + track_cols[t].1 as u16 - 1, + y: 2 + area.y + (scene_rows[s].1 / 96) as u16, + width: track_cols[t].0 as u16, + height: (scene_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)?; + }, + } + + Split::down([ + &|buf: &mut Buffer, area: Rect|state.draw_tracks_header( + buf, area, track_cols.as_slice(), offset), + &|buf: &mut Buffer, area: Rect|state.draw_scene_rows( + buf, area, track_cols.as_slice(), scene_rows.as_slice(), offset), + ]).render(buf, area)?; + for (_, y) in scene_rows.iter() { + let y = area.y + (*y / 96) as u16 + 1; + for x in area.x..area.width+area.y-2 { + let cell = buf.get_mut(x, y); + cell.modifier = Modifier::UNDERLINED; + cell.underline_color = Color::Rgb(0, 0, 0); + } + } + Ok(area) } - fn draw_horizontal (&self, buf: &mut Buffer, area: Rect) -> Usually { - let style3 = Some(Style::default().yellow().bold().not_dim()); - let style4 = Some(Style::default().dim()); - let style5 = Some(Style::default().white().bold().not_dim()); - Split::right([ + /// Rendering with vertical time + impl Arranger { + pub fn draw_tracks_header ( + &self, + buf: &mut Buffer, + area: Rect, + track_cols: &[(usize, usize)], + offset: u16, + ) -> Usually { + let Rect { y, width, .. } = area; + for (track, (_, x)) in self.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 draw_scene_rows ( + &self, + buf: &mut Buffer, + area: Rect, + track_cols: &[(usize, usize)], + scene_rows: &[(usize, usize)], + offset: u16, + ) -> Usually { + let black = Some(Style::default().fg(Color::Rgb(0, 0, 0))); + 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 self.scenes.iter().zip(scene_rows) { + if y > height { + break + } + let h = (pulses / 96) as u16; + let area = Rect { x: area.x, y, width: area.width, height: h.min(area.height - y) }; + self.draw_scene_row(buf, area, scene, track_cols, offset)?; + y = y + h + } + Ok(area) + } + + fn draw_scene_row ( + &self, + buf: &mut Buffer, + area: Rect, + scene: &Scene, + track_cols: &[(usize, usize)], + offset: u16 + ) -> Usually { + let Rect { y, width, .. } = area; + let tracks = self.tracks.as_ref(); + let playing = scene.is_playing(self.tracks.as_ref()); + (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 + } + let tracks: &[Track] = self.tracks.as_ref(); + 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, + }, Color::Rgb(40,80,50)); + } + } + } + } + Ok((scene.pulses(tracks) / 96) as u16) + } + + } +} + +mod draw_horizontal { + use crate::{core::*, view::*}; + use super::{Arranger, track::*}; + + pub fn draw (state: &Arranger, buf: &mut Buffer, area: Rect) -> Usually { + let area = Split::right([ // Track name - &|buf: &mut Buffer, mut area: Rect|{ - area.x = area.x + 1; - let mut width = 0; - for y in 0..area.height { - if y == 0 { - //"MIX".blit(buf, area.x + 1, area.y + y, style1)?; - } else if y % 2 == 1 { - let index = y as usize / 2; - if let Some(track) = self.tracks.get(index) { - width = width.max( - track.name.blit(buf, area.x + 1, area.y + y, style5)?.width - ); - if self.selected == ArrangerFocus::Track(index) { - "".blit(buf, area.x, area.y + y, style3)?; - } + &|buf: &mut Buffer, area: Rect|state.draw_track_name_column(buf, area), + &|buf: &mut Buffer, area: Rect|state.draw_track_mon_column(buf, area), + &|buf: &mut Buffer, area: Rect|state.draw_track_rec_column(buf, area), + &|buf: &mut Buffer, area: Rect|state.draw_track_ovr_column(buf, area), + &|buf: &mut Buffer, area: Rect|state.draw_track_del_column(buf, area), + &|buf: &mut Buffer, area: Rect|state.draw_track_gain_column(buf, area), + &|buf: &mut Buffer, area: Rect|state.draw_track_scenes_column(buf, area), + ]).render(buf, area)?; + fill_bg(buf, area, Nord::bg_lo(state.focused, state.entered)); + Ok(area) + } + + /// Rendering with horizontal time + impl Arranger { + pub fn draw_track_name_column (&self, buf: &mut Buffer, mut area: Rect) -> Usually { + area.width = 3 + 5.max(track_name_max_len(self.tracks.as_slice())) as u16; + let dim = Some(Style::default().dim()); + let yellow = Some(Style::default().yellow().bold().not_dim()); + let white = Some(Style::default().white().bold().not_dim()); + 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) = self.tracks.get(index) { + let style = if self.selected.track() == Some(index) { + yellow } else { - area.height = y; - break - } + white + }; + format!(" {index:>02} ").blit(buf, area.x, area.y + y, style)?; + track.name.blit(buf, area.x + 4, area.y + y, style)?; } } - area.width = width + 1; - Ok(area) - }, + } + Ok(area) + } - &|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 == 1 { - let index = y as usize / 2; - if let Some(track) = self.tracks.get(index) { - " MON ".blit(buf, area.x, area.y + y, if track.monitoring { - Some(Style::default().not_dim().green().bold()) - } else { - style4 - })?; + pub fn draw_track_mon_column (&self, buf: &mut Buffer, mut area: Rect) -> Usually { + let style4 = Some(Style::default().dim()); + 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) = self.tracks.get(index) { + " MON ".blit(buf, area.x, area.y + y, if track.monitoring { + Some(Style::default().not_dim().green().bold()) } else { - area.height = y; - break - } - } - } - area.width = 4; - Ok(area) - }, - - &|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 == 1 { - let index = y as usize / 2; - if let Some(track) = self.tracks.get(index) { - " REC ".blit(buf, area.x, area.y + y, if track.recording { - Some(Style::default().not_dim().red().bold()) - } else { - style4 - })?; - } else { - area.height = y; - break - } - } - } - area.width = 4; - Ok(area) - }, - - &|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 == 1 { - let index = y as usize / 2; - if let Some(track) = self.tracks.get(index) { - " OVR ".blit(buf, area.x, area.y + y, if track.overdub { - Some(Style::default().not_dim().yellow().bold()) - } else { - style4 - })?; - } else { - area.height = y; - break - } - } - } - area.width = 4; - Ok(area) - }, - - &|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 == 1 { - let index = y as usize / 2; - if let Some(_) = self.tracks.get(index) { - " DEL ".blit(buf, area.x, area.y + y, style4)?; - } else { - area.height = y; - break - } - } - } - area.width = 4; - Ok(area) - }, - - &|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 == 1 { - let index = y as usize / 2; - if let Some(_) = self.tracks.get(index) { - " +0.0 ".blit(buf, area.x, area.y + y, style4)?; - } else { - area.height = y; - break - } - } - } - area.width = 7; - Ok(area) - }, - - // Scene columns - &|buf: &mut Buffer, area: Rect|{ - let mut x2 = 0; - let Rect { x, y, height, .. } = area; - for (_scene_index, scene) in self.scenes.iter().enumerate() { - let active_scene = false;//self.selected == ArrangerFocus::Scene(i) || cursor.1 > 0 && self.cursor.1 - 1 == i; - let sep = Some(if active_scene { - Style::default().yellow().not_dim() + style4 + })?; } else { - Style::default().dim() - }); - "╷".blit(buf, x + x2, y, sep)?; - for y in y+1..y+height-1 { - "┊".blit(buf, x + x2, y, sep)?; + area.height = y; + break } - "╵".blit(buf, x + x2, y+height-1, sep)?; - - let mut x3 = scene.name.len() as u16; - scene.name.blit(buf, x + x2, y, Some(Style::default().bold().not_dim()))?; - for (i, clip) in scene.clips.iter().enumerate() { - let active_track = false;//self.cursor.0 > 0 && self.cursor.0 - 1 == i; - if let Some(clip) = clip { - let y2 = y + 1 + i as u16 * 2; - let label = match self.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 }) - }, - ]).render(buf, area) - } + } + area.width = 4; + Ok(area) + } -} + pub fn draw_track_rec_column (&self, buf: &mut Buffer, mut area: Rect) -> Usually { + let style4 = Some(Style::default().dim()); + 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) = self.tracks.get(index) { + " REC ".blit(buf, area.x, area.y + y, if track.recording { + Some(Style::default().not_dim().red().bold()) + } else { + style4 + })?; + } else { + area.height = y; + break + } + } + } + area.width = 4; + Ok(area) + } -/// 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 draw_track_ovr_column (&self, buf: &mut Buffer, mut area: Rect) -> Usually { + let style4 = Some(Style::default().dim()); + 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) = self.tracks.get(index) { + " OVR ".blit(buf, area.x, area.y + y, if track.overdub { + Some(Style::default().not_dim().yellow().bold()) + } else { + style4 + })?; + } else { + area.height = y; + break + } + } + } + area.width = 4; + Ok(area) + } -/// 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.track_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) - } -} + pub fn draw_track_del_column (&self, buf: &mut Buffer, mut area: Rect) -> Usually { + let style4 = Some(Style::default().dim()); + 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(_) = self.tracks.get(index) { + " DEL ".blit(buf, area.x, area.y + y, style4)?; + } else { + area.height = y; + break + } + } + } + area.width = 4; + Ok(area) + } -/// Phrase management methods -impl Arranger { - pub fn phrase (&self) -> Option<&Arc>> { - let track_id = self.selected.track()?; - self.tracks.get(track_id)?.phrases.get((*self.scene()?.clips.get(track_id)?)?) - } - //pub fn phrase_mut (&mut self) -> Option<&mut Phrase> { - //let track_id = self.selected.track()?; - //let clip = *self.scene()?.clips.get(track_id)?; - //self.tracks.get_mut(track_id)?.phrases.get_mut(clip?) - //} - pub fn phrase_next (&mut self) { - unimplemented!(); - //if let Some((track_index, track)) = self.track_mut() { - //let phrases = track.phrases.len(); - //if let Some((_, scene)) = self.scene_mut() { - //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); - //} - //} - //} - } - pub fn phrase_prev (&mut self) { - unimplemented!(); - //if let Some((track_index, track)) = self.track_mut() { - //let phrases = track.phrases.len(); - //if let Some((_, scene)) = self.scene_mut() { - //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); - //} - //} - //} - } -} + pub fn draw_track_gain_column (&self, buf: &mut Buffer, mut area: Rect) -> Usually { + let style4 = Some(Style::default().dim()); + 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(_) = self.tracks.get(index) { + " +0.0 ".blit(buf, area.x, area.y + y, style4)?; + } else { + area.height = y; + break + } + } + } + area.width = 7; + Ok(area) + } -#[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 { - match self { - Self::Clip(t, _) => Some(*t), - Self::Track(t) => Some(*t), - _ => None + pub fn draw_track_scenes_column (&self, buf: &mut Buffer, area: Rect) -> Usually { + let mut x2 = 0; + let Rect { x, y, height, .. } = area; + for (scene_index, scene) in self.scenes.iter().enumerate() { + let active_scene = self.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 = self.selected.track() == Some(i); + if let Some(clip) = clip { + let y2 = y + 2 + i as u16 * 2; + let label = match self.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 }) } } - 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), +} + +mod focus { + #[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 } } } - 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) + + /// Track focus methods + impl ArrangerFocus { + pub fn track (&self) -> Option { + 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 { + 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)) + } } } } } -/// Scene focus methods -impl ArrangerFocus { - pub fn scene (&self) -> Option { - match self { - Self::Clip(_, s) => Some(*s), - Self::Scene(s) => Some(*s), - _ => None +mod scene { + use crate::{core::*, model::Track}; + use super::Arranger; + + /// A collection of phrases to play on each track. + pub struct Scene { + pub name: String, + pub clips: Vec>, + } + + impl Scene { + pub fn new (name: impl AsRef, clips: impl AsRef<[Option]>) -> 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_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_name_max_len (scenes: &[Scene]) -> usize { + scenes.iter() + .map(|s|s.name.len()) + .fold(0, usize::max) } - 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)) - } + + 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) + } + } +} + +mod track { + use crate::{core::*, model::Track}; + 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 + } +} + +mod phrase { + use crate::{core::*, devices::sequencer::Phrase}; + use super::Arranger; + + /// Phrase management methods + impl Arranger { + pub fn phrase (&self) -> Option<&Arc>> { + 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(()) + }) + }); } } } diff --git a/src/devices/sequencer.rs b/src/devices/sequencer.rs index 69525d4a..34a8237a 100644 --- a/src/devices/sequencer.rs +++ b/src/devices/sequencer.rs @@ -44,6 +44,87 @@ pub const KEYMAP_SEQUENCER: &'static [KeyBinding] = keymap!(App { */ }); +pub type PhraseData = Vec>; + +#[derive(Debug)] +/// A MIDI sequence. +pub struct Phrase { + pub name: String, + pub length: usize, + pub notes: PhraseData, + pub looped: Option<(usize, usize)>, + /// Immediate note-offs in view + 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) -> 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, + (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, + _ => {} + } + } + } + } +} + /// Phrase editor. pub struct Sequencer { pub mode: bool, diff --git a/src/edn.rs b/src/edn.rs index 83e64304..42de374f 100644 --- a/src/edn.rs +++ b/src/edn.rs @@ -14,7 +14,11 @@ //! * [LV2Plugin::load_edn] use crate::{core::*, model::*, App}; -use crate::devices::sampler::{Sampler, Sample, read_sample_data}; +use crate::devices::{ + arranger::Scene, + sequencer::Phrase, + sampler::{Sampler, Sample, read_sample_data} +}; use crate::devices::plugin::{Plugin, LV2Plugin}; use clojure_reader::{edn::{read, Edn}, error::Error as EdnError}; diff --git a/src/jack/device.rs b/src/jack/device.rs index 59a60ed8..8a01b42e 100644 --- a/src/jack/device.rs +++ b/src/jack/device.rs @@ -12,6 +12,11 @@ pub struct JackDevice { /// The "real" readable/writable `Port`s are owned by the `state`. pub ports: UnownedJackPorts, } +impl std::fmt::Debug for JackDevice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("JackDevice").field("ports", &self.ports).finish() + } +} render!(JackDevice |self, buf, area| { self.state.read().unwrap().render(buf, area) }); diff --git a/src/jack/ports.rs b/src/jack/ports.rs index 7404d09d..9d2550dd 100644 --- a/src/jack/ports.rs +++ b/src/jack/ports.rs @@ -12,6 +12,7 @@ pub struct JackPorts { #[derive(Default)] /// Collection of JACK ports as [Unowned]. +#[derive(Debug)] pub struct UnownedJackPorts { pub audio_ins: BTreeMap>, pub midi_ins: BTreeMap>, diff --git a/src/model.rs b/src/model.rs index 757e4225..54935202 100644 --- a/src/model.rs +++ b/src/model.rs @@ -123,7 +123,7 @@ impl App { #[derive(PartialEq, Clone, Copy)] pub enum AppFocus { /// The transport is selected. - Transport, + Transport, /// The arranger is selected. Arranger, /// The sequencer is selected. @@ -156,6 +156,7 @@ impl AppFocus { } /// A sequencer track. +#[derive(Debug)] pub struct Track { pub name: String, /// Play input through output. @@ -366,102 +367,6 @@ impl Track { }} } -pub type PhraseData = Vec>; - -#[derive(Debug)] -/// A MIDI sequence. -pub struct Phrase { - pub name: String, - pub length: usize, - pub notes: PhraseData, - pub looped: Option<(usize, usize)>, - /// Immediate note-offs in view - 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) -> 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, - (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, - _ => {} - } - } - } - } -} - -/// A collection of phrases to play on each track. -pub struct Scene { - pub name: String, - pub clips: Vec>, -} - -impl Scene { - pub fn new (name: impl AsRef, clips: impl AsRef<[Option]>) -> Self { - Self { - name: name.as_ref().into(), - clips: clips.as_ref().iter().map(|x|x.clone()).collect() - } - } -} - macro_rules! impl_axis_common { ($A:ident $T:ty) => { impl $A<$T> { pub fn start_inc (&mut self) -> $T {