diff --git a/src/devices/arranger.rs b/src/devices/arranger.rs index 538c2557..4aef282b 100644 --- a/src/devices/arranger.rs +++ b/src/devices/arranger.rs @@ -1,8 +1,10 @@ //! Clip launcher and arrangement editor. use crate::{core::*, model::*}; -use self::focus::ArrangerFocus; -pub use self::scene::Scene; +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] = keymap!(App { @@ -124,725 +126,9 @@ impl Arranger { render!(Arranger |self, buf, area| match self.mode { ArrangerViewMode::Horizontal => - self::draw_horizontal::draw(self, buf, area), + self::arr_draw_h::draw(self, buf, area), ArrangerViewMode::Vertical => - self::draw_vertical::draw_expanded(self, buf, area), + self::arr_draw_v::draw_expanded(self, buf, area), ArrangerViewMode::VerticalCompact => - self::draw_vertical::draw_compact(self, buf, area), + self::arr_draw_v::draw_compact(self, buf, area), }); - -mod draw_vertical { - use crate::{core::*, view::*}; - use super::{Arranger, focus::ArrangerFocus, track::track_clip_name_lengths, scene::{Scene, scene_ppqs, scene_name_max_len}}; - - pub fn draw_expanded (state: &Arranger, buf: &mut Buffer, 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()); - draw(state, buf, area, track_cols.as_slice(), scene_rows.as_slice()) - } - - pub fn draw_compact (state: &Arranger, buf: &mut Buffer, area: Rect) -> Usually { - 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::>(); - 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 { - 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(Color::Rgb(0,0,0))); - 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 = Color::Rgb(0, 0, 0); - } - } - 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(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 state.scenes.iter().zip(scene_rows) { - if y > height { - break - } - let h = 1.max((pulses / 96) as u16); - let area = Rect { x: area.x, y, width: area.width, height: h.min(area.height - y) }; - scene_row(state, buf, area, 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 { - 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, - }, Color::Rgb(60,100,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, mut area: Rect) -> Usually { - 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 }) - } - } -} - -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 } - } - } - - /// 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)) - } - } - } - } -} - -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_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) - } - } -} - -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/arranger/arr_draw_h.rs b/src/devices/arranger/arr_draw_h.rs new file mode 100644 index 00000000..757d0bc1 --- /dev/null +++ b/src/devices/arranger/arr_draw_h.rs @@ -0,0 +1,199 @@ +use crate::{core::*, view::*}; +use super::{Arranger, arr_track::*}; + +pub fn draw (state: &Arranger, buf: &mut Buffer, mut area: Rect) -> Usually { + 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 }) + } +} diff --git a/src/devices/arranger/arr_draw_v.rs b/src/devices/arranger/arr_draw_v.rs new file mode 100644 index 00000000..2687cce8 --- /dev/null +++ b/src/devices/arranger/arr_draw_v.rs @@ -0,0 +1,221 @@ +use crate::{core::*, view::*}; +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 { + 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 { + 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::>(); + 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 { + 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(Color::Rgb(0,0,0))); + 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 = Color::Rgb(0, 0, 0); + } + } + 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(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 state.scenes.iter().zip(scene_rows) { + if y > height { + break + } + let h = 1.max((pulses / 96) as u16); + let area = Rect { x: area.x, y, width: area.width, height: h.min(area.height - y) }; + scene_row(state, buf, area, 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 { + 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, + }, Color::Rgb(60,100,50)); + } + } + } + } + Ok((scene.pulses(tracks) / 96) as u16) +} diff --git a/src/devices/arranger/arr_focus.rs b/src/devices/arranger/arr_focus.rs new file mode 100644 index 00000000..df05d15a --- /dev/null +++ b/src/devices/arranger/arr_focus.rs @@ -0,0 +1,95 @@ +#[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 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)) + } + } + } +} diff --git a/src/devices/arranger/arr_phrase.rs b/src/devices/arranger/arr_phrase.rs new file mode 100644 index 00000000..209e87fc --- /dev/null +++ b/src/devices/arranger/arr_phrase.rs @@ -0,0 +1,56 @@ +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/arranger/arr_scene.rs b/src/devices/arranger/arr_scene.rs new file mode 100644 index 00000000..ee6f6427 --- /dev/null +++ b/src/devices/arranger/arr_scene.rs @@ -0,0 +1,79 @@ +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_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) + } +} diff --git a/src/devices/arranger/arr_track.rs b/src/devices/arranger/arr_track.rs new file mode 100644 index 00000000..f9c16218 --- /dev/null +++ b/src/devices/arranger/arr_track.rs @@ -0,0 +1,53 @@ +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 +}