diff --git a/crates/tek_sequencer/src/arranger.rs b/crates/tek_sequencer/src/arranger.rs new file mode 100644 index 00000000..57d5fa17 --- /dev/null +++ b/crates/tek_sequencer/src/arranger.rs @@ -0,0 +1,455 @@ +use crate::*; + +/// Represents the tracks and scenes of the composition. +pub struct Arranger { + /// Name of arranger + pub name: Arc>, + /// Collection of tracks. + pub tracks: Vec>, + /// Collection of scenes. + pub scenes: Vec, + /// Currently selected element. + pub selected: ArrangerFocus, + /// Display mode of arranger + pub mode: ArrangerViewMode, + /// Slot for modal dialog displayed on top of app. + pub modal: Option>>, + /// Whether the arranger is currently focused + pub focused: bool +} + +impl Arranger { + pub fn new (name: &str) -> Self { + Self { + name: Arc::new(RwLock::new(name.into())), + mode: ArrangerViewMode::Vertical(2), + selected: ArrangerFocus::Clip(0, 0), + scenes: vec![], + tracks: vec![], + modal: None, + focused: false + } + } + pub fn activate (&mut self) { + match self.selected { + ArrangerFocus::Scene(s) => { + for (track_index, track) in self.tracks.iter_mut().enumerate() { + track.playing_phrase = self.scenes[s].clips[track_index]; + track.reset = true; + } + }, + ArrangerFocus::Clip(t, s) => { + self.tracks[t].playing_phrase = self.scenes[s].clips[t]; + self.tracks[t].reset = true; + }, + _ => {} + } + } + pub fn sequencer (&self) -> Option<&Sequencer> { + self.selected.track() + .map(|track|self.tracks.get(track)) + .flatten() + } + pub fn sequencer_mut (&mut self) -> Option<&mut Sequencer> { + self.selected.track() + .map(|track|self.tracks.get_mut(track)) + .flatten() + } + pub fn show_phrase (&mut self) { + let (scene, track) = (self.selected.scene(), self.selected.track()); + if let (Some(scene_index), Some(track_index)) = (scene, track) { + let scene = self.scenes.get(scene_index); + let track = self.tracks.get_mut(track_index); + if let (Some(scene), Some(track)) = (scene, track) { + track.viewing_phrase = scene.clips[track_index] + } + } + } + pub fn is_first_row (&self) -> bool { + let selected = self.selected; + selected.is_mix() || selected.is_track() + } + pub fn is_last_row (&self) -> bool { + let selected = self.selected; + (self.scenes.len() == 0 && (selected.is_mix() || selected.is_track())) || match selected { + ArrangerFocus::Scene(s) => + s == self.scenes.len() - 1, + ArrangerFocus::Clip(_, s) => + s == self.scenes.len() - 1, + _ => false + } + } + pub fn track (&self) -> Option<&Sequencer> { + self.selected.track().map(|t|self.tracks.get(t)).flatten() + } + pub fn track_mut (&mut self) -> Option<&mut Sequencer> { + 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 Sequencer> { + self.tracks.push(name.map_or_else( + || Sequencer::new(&self.track_default_name()), + |name| Sequencer::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 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) + } + 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_del (&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))) + .map(|(track_index, _)|{ + scene_index + .and_then(|index|self.scenes.get_mut(index)) + .map(|scene|scene.clips[track_index] = None); + }); + } + 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(()) + }) + }); + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#[derive(PartialEq, Clone, Copy)] +/// 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 description ( + &self, + tracks: &Vec>, + scenes: &Vec, + ) -> String { + format!("Selected: {}", match self { + Self::Mix => format!("Everything"), + Self::Track(t) => if let Some(track) = tracks.get(*t) { + format!("T{t}: {}", &track.name.read().unwrap()) + } else { + format!("T??") + }, + Self::Scene(s) => if let Some(scene) = scenes.get(*s) { + format!("S{s}: {}", &scene.name.read().unwrap()) + } else { + format!("S??") + }, + Self::Clip(t, s) => if let (Some(track), Some(scene)) = ( + tracks.get(*t), + scenes.get(*s), + ) { + if let Some(Some(slot)) = scene.clips.get(*t) { + if let Some(clip) = track.phrases.get(*slot) { + format!("T{t} S{s} C{slot} ({})", &clip.read().unwrap().name.read().unwrap()) + } else { + format!("T{t} S{s}: Empty") + } + } else { + format!("T{t} S{s}: Empty") + } + } else { + format!("T{t} S{s}: Empty") + } + }) + } + pub fn is_mix (&self) -> bool { + match self { Self::Mix => true, _ => false } + } + 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 (&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) + } + } + } + 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)) + } + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Display mode of arranger +#[derive(PartialEq)] +pub enum ArrangerViewMode { Horizontal, Vertical(usize) } +/// Arranger display mode can be cycled +impl ArrangerViewMode { + /// Cycle arranger display mode + pub fn to_next (&mut self) { + *self = match self { + Self::Horizontal => Self::Vertical(1), + Self::Vertical(1) => Self::Vertical(2), + Self::Vertical(2) => Self::Vertical(2), + Self::Vertical(0) => Self::Horizontal, + Self::Vertical(_) => Self::Vertical(0), + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +pub struct VerticalArranger<'a, E: Engine>( + pub &'a Arranger, pub usize +); +pub struct VerticalArrangerGrid<'a>( + pub u16, pub &'a [(usize, usize)], pub &'a [(usize, usize)] +); +pub struct VerticalArrangerCursor<'a>( + pub bool, pub ArrangerFocus, pub u16, pub &'a [(usize, usize)], pub &'a [(usize, usize)], +); + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +pub struct HorizontalArranger<'a, E: Engine>( + pub &'a Arranger +); + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Appears on first run (i.e. if state dir is missing). +pub struct ArrangerRenameModal { + _engine: std::marker::PhantomData, + pub done: bool, + pub target: ArrangerFocus, + pub value: String, + pub result: Arc>, + pub cursor: usize +} + +impl ArrangerRenameModal { + pub fn new (target: ArrangerFocus, value: &Arc>) -> Self { + Self { + _engine: Default::default(), + done: false, + value: value.read().unwrap().clone(), + cursor: value.read().unwrap().len(), + result: value.clone(), + target, + } + } +} + +impl Exit for ArrangerRenameModal { + fn exited (&self) -> bool { self.done } + fn exit (&mut self) { self.done = true } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// A collection of phrases to play on each track. +#[derive(Default)] +pub struct Scene { + pub name: Arc>, + pub clips: Vec>, +} + +impl Scene { + pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually { + let mut name = None; + let mut clips = vec![]; + edn!(edn in args { + Edn::Map(map) => { + let key = map.get(&Edn::Key(":name")); + if let Some(Edn::Str(n)) = key { + name = Some(*n); + } else { + panic!("unexpected key in scene '{name:?}': {key:?}") + } + }, + Edn::Symbol("_") => { + clips.push(None); + }, + Edn::Int(i) => { + clips.push(Some(*i as usize)); + }, + _ => panic!("unexpected in scene '{name:?}': {edn:?}") + }); + let scene = Self::new(name.unwrap_or(""), clips); + Ok(scene) + } + pub fn new (name: impl AsRef, clips: impl AsRef<[Option]>) -> Self { + let name = Arc::new(RwLock::new(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: &[Sequencer]) -> usize { + self.clips.iter().enumerate() + .filter_map(|(i, c)|c + .map(|c|tracks + .get(i) + .map(|track|track + .phrases + .get(c)))) + .filter_map(|p|p) + .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: &[Sequencer]) -> bool { + self.clips.iter().enumerate() + .all(|(track_index, phrase_index)|match phrase_index { + Some(i) => tracks + .get(track_index) + .map(|track|track.playing_phrase == Some(*i)) + .unwrap_or(false), + None => true + }) + } +} diff --git a/crates/tek_sequencer/src/arranger_tui.rs b/crates/tek_sequencer/src/arranger_tui.rs new file mode 100644 index 00000000..0ad65aea --- /dev/null +++ b/crates/tek_sequencer/src/arranger_tui.rs @@ -0,0 +1,629 @@ +use crate::*; + +impl Arranger { + pub fn rename_selected (&mut self) { + self.modal = Some(Box::new(ArrangerRenameModal::new( + self.selected, + &match self.selected { + ArrangerFocus::Mix => self.name.clone(), + ArrangerFocus::Track(t) => self.tracks[t].name.clone(), + ArrangerFocus::Scene(s) => self.scenes[s].name.clone(), + ArrangerFocus::Clip(t, s) => self.tracks[t].phrases[s].read().unwrap().name.clone(), + } + ))); + } +} +impl Focusable for Arranger { + fn is_focused (&self) -> bool { + self.focused + } + fn set_focused (&mut self, focused: bool) { + self.focused = focused + } +} +impl Handle for Arranger { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + if let Some(modal) = self.modal.as_mut() { + let result = modal.handle(from)?; + if from.is_done() { + self.modal = None; + } + return Ok(result) + } + match from.event() { + // mode_switch: switch the display mode + key!(KeyCode::Char('`')) => { + self.mode.to_next() + }, + // cursor_up: move cursor up + key!(KeyCode::Up) => { + match self.mode { + ArrangerViewMode::Horizontal => self.track_prev(), + _ => self.scene_prev(), + }; + self.show_phrase(); + }, + // cursor_down + key!(KeyCode::Down) => { + match self.mode { + ArrangerViewMode::Horizontal => self.track_next(), + _ => self.scene_next(), + }; + self.show_phrase(); + }, + // cursor left + key!(KeyCode::Left) => { + match self.mode { + ArrangerViewMode::Horizontal => self.scene_prev(), + _ => self.track_prev(), + }; + self.show_phrase(); + }, + // cursor right + key!(KeyCode::Right) => { + match self.mode { + ArrangerViewMode::Horizontal => self.scene_next(), + _ => self.track_next(), + }; + self.show_phrase(); + }, + // increment: remove clip + key!(KeyCode::Delete) => { + self.phrase_del(); + self.show_phrase(); + }, + // increment: use next clip here + key!(KeyCode::Char('.')) => { + self.phrase_next(); + self.show_phrase(); + }, + // decrement: use previous next clip here + key!(KeyCode::Char(',')) => { + self.phrase_prev(); + self.show_phrase(); + }, + // decrement: use previous clip here + key!(KeyCode::Enter) => { + self.activate(); + }, + // scene_add: add a new scene + key!(Ctrl-KeyCode::Char('a')) => { + self.scene_add(None)?; + }, + // track_add: add a new scene + key!(Ctrl-KeyCode::Char('t')) => { + self.track_add(None)?; + }, + // rename: add a new scene + key!(KeyCode::Char('n')) => { + self.rename_selected(); + }, + // length: add a new scene + key!(KeyCode::Char('l')) => if let Some(phrase) = self.phrase() { + phrase.write().unwrap().toggle_loop() + }, + // color: set color of item at cursor + key!(KeyCode::Char('c')) => { + todo!(); + }, + _ => return Ok(None) + } + Ok(Some(true)) + } +} +impl Content for Arranger { + type Engine = Tui; + fn content (&self) -> impl Widget { + Layers::new(move |add|{ + match self.mode { + ArrangerViewMode::Horizontal => add(&HorizontalArranger(&self)), + ArrangerViewMode::Vertical(factor) => add(&VerticalArranger(&self, factor)) + }?; + add(&Align::SE(self.selected.description( + &self.tracks, + &self.scenes, + ).as_str())) + }) + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +impl<'a> Content for VerticalArranger<'a, Tui> { + type Engine = Tui; + fn content (&self) -> impl Widget { + let Self(state, factor) = self; + let ppq = 96; + let (cols, rows) = if *factor == 0 {( + track_clip_name_lengths(state.tracks.as_slice()), + scene_ppqs(state.tracks.as_slice(), state.scenes.as_slice()), + )} else {( + track_clip_name_lengths(state.tracks.as_slice()), + (0..=state.scenes.len()).map(|i|(factor*ppq, factor*ppq*i)).collect::>(), + )}; + //let height = rows.last().map(|(w,y)|(y+w)/ppq).unwrap_or(16); + let tracks: &[Sequencer] = state.tracks.as_ref(); + let scenes: &[Scene] = state.scenes.as_ref(); + let offset = 4 + scene_name_max_len(scenes) as u16; + Layers::new(move |add|{ + let rows: &[(usize, usize)] = rows.as_ref(); + let cols: &[(usize, usize)] = cols.as_ref(); + + let track_titles = row!((track, (w, _)) in tracks.iter().zip(cols) => + (&track.name.read().unwrap().as_str() as &dyn Widget) + .min_xy(*w as u16, 2).push_x(offset)); + + let scene_name = |scene, playing: bool, height|row!( + if playing { "▶ " } else { " " }, + (scene as &Scene).name.read().unwrap().as_str(), + ).fixed_xy(offset.saturating_sub(1), height); + + let scene_clip = |scene, track: usize, w: u16, h: u16|Layers::new(move |add|{ + let mut color = Color::Rgb(40, 50, 30); + match (tracks.get(track), (scene as &Scene).clips.get(track)) { + (Some(track), Some(Some(clip))) => match track.phrases.get(*clip) { + Some(phrase) => { + let name = &(phrase as &Arc>).read().unwrap().name; + let name = name.read().unwrap(); + let name = format!("{clip:02} {}", name); + add(&name.as_str().push_x(1).fixed_x(w))?; + if (track as &Sequencer<_>).playing_phrase == Some(*clip) { + color = COLOR_PLAYING + } else { + color = COLOR_BG1 + }; + }, + _ => {} + }, + _ => {} + }; + add(&Background(color)) + }).fixed_xy(w, h); + + let tracks_clips = col!((scene, (pulses, _)) in scenes.iter().zip(rows) => { + let height = 1.max((pulses / 96) as u16); + let playing = scene.is_playing(tracks); + Stack::right(move |add| { + add(&scene_name(scene, playing, height))?; + for (track, (w, _x)) in cols.iter().enumerate() { + add(&scene_clip(scene, track, *w as u16, height))?; + } + Ok(()) + }).fixed_y(height) + }); + + add(&VerticalArrangerGrid(offset, &rows, &cols))?; + add(&VerticalArrangerCursor(state.focused, state.selected, offset, &cols, &rows))?; + add(&col!(track_titles, tracks_clips))?; + Ok(()) + }) + .bg(Color::Rgb(28, 35, 25)) + .border(Lozenge(Style::default() + .bg(Color::Rgb(40, 50, 30)) + .fg(Color::Rgb(70, 80, 50)))) + } +} + +pub fn track_clip_name_lengths (tracks: &[Sequencer]) -> Vec<(usize, usize)> { + let mut total = 0; + let mut lengths: Vec<(usize, usize)> = tracks.iter().map(|track|{ + let len = 4 + track.phrases + .iter() + .fold(track.name.read().unwrap().len(), |len, phrase|{ + len.max(phrase.read().unwrap().name.read().unwrap().len()) + }); + total = total + len; + (len, total - len) + }).collect(); + lengths.push((0, total)); + lengths +} + +pub fn scene_ppqs (tracks: &[Sequencer], scenes: &[Scene]) -> Vec<(usize, usize)> { + let mut total = 0; + let mut scenes: Vec<(usize, usize)> = scenes.iter().map(|scene|{ + let pulses = scene.pulses(tracks).max(96); + total = total + pulses; + (pulses, total - pulses) + }).collect(); + scenes.push((0, total)); + scenes +} + +pub fn scene_name_max_len (scenes: &[Scene]) -> usize { + scenes.iter() + .map(|s|s.name.read().unwrap().len()) + .fold(0, usize::max) +} + +impl<'a> Widget for VerticalArrangerGrid<'a> { + type Engine = Tui; + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + let area = to.area(); + let Self(offset, rows, cols) = self; + let style = Some(Style::default().fg(COLOR_SEPARATOR)); + for (_, x) in cols.iter() { + let x = offset + area.x() + *x as u16 - 1; + for y in area.y()..area.y2() { + to.blit(&"▎", x, y, style); + } + } + for (_, y) in rows.iter() { + let y = area.y() + (*y / 96) as u16 + 1; + if y >= to.buffer.area.height { + break + } + for x in area.x()..area.x2().saturating_sub(2) { + if x < to.buffer.area.x && y < to.buffer.area.y { + let cell = to.buffer.get_mut(x, y); + cell.modifier = Modifier::UNDERLINED; + cell.underline_color = COLOR_SEPARATOR; + } + } + } + Ok(()) + } +} + +impl<'a> Widget for VerticalArrangerCursor<'a> { + type Engine = Tui; + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + let area = to.area(); + let Self(focused, selected, offset, cols, rows) = *self; + let get_track_area = |t: usize| [ + offset + area.x() + cols[t].1 as u16 - 1, + area.y(), + cols[t].0 as u16, + area.h() + ]; + let get_scene_area = |s: usize| [ + area.x(), + 2 + area.y() + (rows[s].1 / 96) as u16, + area.w(), + (rows[s].0 / 96) as u16 + ]; + let get_clip_area = |t: usize, s: usize| [ + offset + area.x() + cols[t].1 as u16 - 1, + 2 + area.y() + (rows[s].1 / 96) as u16, + cols[t].0 as u16, + (rows[s].0 / 96) as u16 + ]; + let mut track_area: Option<[u16;4]> = None; + let mut scene_area: Option<[u16;4]> = None; + let mut clip_area: Option<[u16;4]> = None; + let area = match selected { + ArrangerFocus::Mix => { + if focused { + to.fill_bg(area, Color::Rgb(40, 50, 30)); + } + area + }, + ArrangerFocus::Track(t) => { + track_area = Some(get_track_area(t)); + area + }, + ArrangerFocus::Scene(s) => { + scene_area = Some(get_scene_area(s)); + area + }, + ArrangerFocus::Clip(t, s) => { + track_area = Some(get_track_area(t)); + scene_area = Some(get_scene_area(s)); + clip_area = Some(get_clip_area(t, s)); + area + }, + }; + if let Some([x, y, width, height]) = track_area { + to.fill_fg([x, y, 1, height], Color::Rgb(70, 80, 50)); + to.fill_fg([x + width, y, 1, height], Color::Rgb(70, 80, 50)); + } + if let Some([_, y, _, height]) = scene_area { + to.fill_ul([area.x(), y - 1, area.w(), 1], Color::Rgb(70, 80, 50)); + to.fill_ul([area.x(), y + height - 1, area.w(), 1], Color::Rgb(70, 80, 50)); + } + if focused { + if let Some(clip_area) = clip_area { + to.render_in(clip_area, &CORNERS)?; + to.fill_bg(clip_area, Color::Rgb(40, 50, 30)); + } else if let Some(track_area) = track_area { + to.render_in(track_area.clip_h(2), &CORNERS)?; + to.fill_bg(track_area, Color::Rgb(40, 50, 30)); + } else if let Some(scene_area) = scene_area { + to.render_in(scene_area.clip_w(offset-1), &CORNERS)?; + to.fill_bg(scene_area, Color::Rgb(40, 50, 30)); + } + } + Ok(()) + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +impl<'a> Content for HorizontalArranger<'a, Tui> { + type Engine = Tui; + fn content (&self) -> impl Widget { + let Arranger { tracks, focused, selected, scenes, .. } = self.0; + let _tracks = tracks.as_slice(); + lay!( + focused.then_some(Background(Color::Rgb(40, 50, 30))), + row!( + // name + CustomWidget::new(|_|{ + todo!() + }, |_: &mut TuiOutput|{ + todo!() + //let Self(tracks, selected) = self; + //let yellow = Some(Style::default().yellow().bold().not_dim()); + //let white = Some(Style::default().white().bold().not_dim()); + //let area = to.area(); + //let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()]; + //let offset = 0; // track scroll offset + //for y in 0..area.h() { + //if y == 0 { + //to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?; + //} else if y % 2 == 0 { + //let index = (y as usize - 2) / 2 + offset; + //if let Some(track) = tracks.get(index) { + //let selected = selected.track() == Some(index); + //let style = if selected { yellow } else { white }; + //to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?; + //to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?; + //} + //} + //} + //Ok(Some(area)) + }), + // monitor + CustomWidget::new(|_|{ + todo!() + }, |_: &mut TuiOutput|{ + todo!() + //let Self(tracks) = self; + //let mut area = to.area(); + //let on = Some(Style::default().not_dim().green().bold()); + //let off = Some(DIM); + //area.x += 1; + //for y in 0..area.h() { + //if y == 0 { + ////" MON ".blit(to.buffer, area.x, area.y + y, style2)?; + //} else if y % 2 == 0 { + //let index = (y as usize - 2) / 2; + //if let Some(track) = tracks.get(index) { + //let style = if track.monitoring { on } else { off }; + //to.blit(&" MON ", area.x(), area.y() + y, style)?; + //} else { + //area.height = y; + //break + //} + //} + //} + //area.width = 4; + //Ok(Some(area)) + }), + // record + CustomWidget::new(|_|{ + todo!() + }, |_: &mut TuiOutput|{ + todo!() + //let Self(tracks) = self; + //let mut area = to.area(); + //let on = Some(Style::default().not_dim().red().bold()); + //let off = Some(Style::default().dim()); + //area.x += 1; + //for y in 0..area.h() { + //if y == 0 { + ////" REC ".blit(to.buffer, area.x, area.y + y, style2)?; + //} else if y % 2 == 0 { + //let index = (y as usize - 2) / 2; + //if let Some(track) = tracks.get(index) { + //let style = if track.recording { on } else { off }; + //to.blit(&" REC ", area.x(), area.y() + y, style)?; + //} else { + //area.height = y; + //break + //} + //} + //} + //area.width = 4; + //Ok(Some(area)) + }), + // overdub + CustomWidget::new(|_|{ + todo!() + }, |_: &mut TuiOutput|{ + todo!() + //let Self(tracks) = self; + //let mut area = to.area(); + //let on = Some(Style::default().not_dim().yellow().bold()); + //let off = Some(Style::default().dim()); + //area.x = area.x + 1; + //for y in 0..area.h() { + //if y == 0 { + ////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?; + //} else if y % 2 == 0 { + //let index = (y as usize - 2) / 2; + //if let Some(track) = tracks.get(index) { + //to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub { + //on + //} else { + //off + //})?; + //} else { + //area.height = y; + //break + //} + //} + //} + //area.width = 4; + //Ok(Some(area)) + }), + // erase + CustomWidget::new(|_|{ + todo!() + }, |_: &mut TuiOutput|{ + todo!() + //let Self(tracks) = self; + //let mut area = to.area(); + //let off = Some(Style::default().dim()); + //area.x = area.x + 1; + //for y in 0..area.h() { + //if y == 0 { + ////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?; + //} else if y % 2 == 0 { + //let index = (y as usize - 2) / 2; + //if let Some(_) = tracks.get(index) { + //to.blit(&" DEL ", area.x(), area.y() + y, off)?; + //} else { + //area.height = y; + //break + //} + //} + //} + //area.width = 4; + //Ok(Some(area)) + }), + // gain + CustomWidget::new(|_|{ + todo!() + }, |_: &mut TuiOutput|{ + todo!() + //let Self(tracks) = self; + //let mut area = to.area(); + //let off = Some(Style::default().dim()); + //area.x = area.x() + 1; + //for y in 0..area.h() { + //if y == 0 { + ////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?; + //} else if y % 2 == 0 { + //let index = (y as usize - 2) / 2; + //if let Some(_) = tracks.get(index) { + //to.blit(&" +0.0 ", area.x(), area.y() + y, off)?; + //} else { + //area.height = y; + //break + //} + //} + //} + //area.width = 7; + //Ok(Some(area)) + }), + // scenes + CustomWidget::new(|_|{ + todo!() + }, |to: &mut TuiOutput|{ + let Arranger { tracks, scenes, selected, .. } = self.0; + let area = to.area(); + let mut x2 = 0; + let [x, y, _, height] = area; + for (scene_index, scene) in scenes.iter().enumerate() { + let active_scene = 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 { + to.blit(&"│", x + x2, y, sep); + } + let name = scene.name.read().unwrap(); + let mut x3 = name.len() as u16; + to.blit(&*name, x + x2, y, sep); + for (i, clip) in scene.clips.iter().enumerate() { + let active_track = selected.track() == Some(i); + if let Some(clip) = clip { + let y2 = y + 2 + i as u16 * 2; + let label = match tracks[i].phrases.get(*clip) { + Some(phrase) => &format!("{}", phrase.read().unwrap().name.read().unwrap()), + None => "...." + }; + to.blit(&label, 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(Some([x, y, x2, height])) + Ok(()) + }), + ) + ) + } +} + +pub fn track_name_max_len (tracks: &[Sequencer]) -> usize { + tracks.iter() + .map(|s|s.name.read().unwrap().len()) + .fold(0, usize::max) +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +impl Content for ArrangerRenameModal { + type Engine = Tui; + fn content (&self) -> impl Widget { + todo!(); + Layers::new(|add|{Ok(())}) + //let area = to.area(); + //let y = area.y() + area.h() / 2; + //let bg_area = [1, y - 1, area.w() - 2, 3]; + //to.fill_bg(bg_area, COLOR_BG1); + //Lozenge(Style::default().bold().white().dim()).draw(to.with_rect(bg_area)); + //let label = match self.target { + //ArrangerFocus::Mix => "Rename project:", + //ArrangerFocus::Track(_) => "Rename track:", + //ArrangerFocus::Scene(_) => "Rename scene:", + //ArrangerFocus::Clip(_, _) => "Rename clip:", + //}; + //let style = Some(Style::default().not_bold().white().not_dim()); + //to.blit(&label, area.x() + 3, y, style); + //let style = Some(Style::default().bold().white().not_dim()); + //to.blit(&self.value, area.x() + 3 + label.len() as u16 + 1, y, style); + //let style = Some(Style::default().bold().white().not_dim().reversed()); + //to.blit(&"▂", area.x() + 3 + label.len() as u16 + 1 + self.cursor as u16, y, style); + //Ok(Some(area)) + //Ok(()) + } +} + +impl Handle for ArrangerRenameModal { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + match from.event() { + TuiEvent::Input(Event::Key(k)) => { + match k.code { + KeyCode::Esc => { + self.exit(); + }, + KeyCode::Enter => { + *self.result.write().unwrap() = self.value.clone(); + self.exit(); + }, + KeyCode::Left => { + self.cursor = self.cursor.saturating_sub(1); + }, + KeyCode::Right => { + self.cursor = self.value.len().min(self.cursor + 1) + }, + KeyCode::Backspace => { + let last = self.value.len().saturating_sub(1); + self.value = format!("{}{}", + &self.value[0..self.cursor.min(last)], + &self.value[self.cursor.min(last)..last] + ); + self.cursor = self.cursor.saturating_sub(1) + } + KeyCode::Char(c) => { + self.value.insert(self.cursor, c); + self.cursor = self.value.len().min(self.cursor + 1) + }, + _ => {} + } + Ok(Some(true)) + }, + _ => Ok(None), + } + } +} diff --git a/crates/tek_sequencer/src/lib.rs b/crates/tek_sequencer/src/lib.rs index dca60713..f5cc0e8d 100644 --- a/crates/tek_sequencer/src/lib.rs +++ b/crates/tek_sequencer/src/lib.rs @@ -6,4 +6,27 @@ pub(crate) use tek_core::midly::{num::u7, live::LiveEvent, MidiMessage}; pub(crate) use tek_core::jack::*; pub(crate) use std::sync::{Arc, RwLock}; -submod! { sequencer sequencer_tui } +submod! { + arranger arranger_tui + sequencer sequencer_tui + transport transport_tui +} + +pub const CORNERS: CornersTall = CornersTall(NOT_DIM_GREEN); + +tui_style!(GRAY_DIM = + Some(Color::Gray), None, None, Modifier::DIM, Modifier::empty()); +tui_style!(GRAY_NOT_DIM_BOLD = + Some(Color::Gray), None, None, Modifier::BOLD, Modifier::DIM); +tui_style!(NOT_DIM_BOLD = + None, None, None, Modifier::BOLD, Modifier::DIM); +tui_style!(NOT_DIM_GREEN = + Some(Color::Rgb(96, 255, 32)), Some(COLOR_BG1), None, Modifier::empty(), Modifier::DIM); +tui_style!(NOT_DIM = + None, None, None, Modifier::empty(), Modifier::DIM); +tui_style!(WHITE_NOT_DIM_BOLD = + Some(Color::White), None, None, Modifier::BOLD, Modifier::DIM); +tui_style!(STYLE_LABEL = + Some(Color::Reset), None, None, Modifier::empty(), Modifier::BOLD); +tui_style!(STYLE_VALUE = + Some(Color::White), None, None, Modifier::BOLD, Modifier::DIM); diff --git a/crates/tek_sequencer/src/main_arranger.rs b/crates/tek_sequencer/src/main_arranger.rs index 86c2dd86..df1a04c4 100644 --- a/crates/tek_sequencer/src/main_arranger.rs +++ b/crates/tek_sequencer/src/main_arranger.rs @@ -67,22 +67,6 @@ struct ArrangerStandalone { /// Focus target that passes events down to sequencer sequencer_proxy: SequencerProxy, } -impl ArrangerStandalone { - fn sequencer (&self) -> Option<&Sequencer> { - if self.show_sequencer.is_some() { - self.arranger.sequencer() - } else { - None - } - } - fn sequencer_mut (&mut self) -> Option<&mut Sequencer> { - if self.show_sequencer.is_some() { - self.arranger.sequencer_mut() - } else { - None - } - } -} /// The standalone arranger consists of transport, clip grid, and sequencer. impl Content for ArrangerStandalone { type Engine = Tui; diff --git a/crates/tek_sequencer/src/sequencer.rs b/crates/tek_sequencer/src/sequencer.rs index 3dffe3e6..335fe9d4 100644 --- a/crates/tek_sequencer/src/sequencer.rs +++ b/crates/tek_sequencer/src/sequencer.rs @@ -1,463 +1,182 @@ use crate::*; -/////////////////////////////////////////////////////////////////////////////////////////////////// +/// A collection of MIDI messages. +pub type PhraseData = Vec>; -/// Represents the tracks and scenes of the composition. -pub struct Arranger { - /// Name of arranger - pub name: Arc>, - /// Collection of tracks. - pub tracks: Vec>, - /// Collection of scenes. - pub scenes: Vec, - /// Currently selected element. - pub selected: ArrangerFocus, - /// Display mode of arranger - pub mode: ArrangerViewMode, - /// Slot for modal dialog displayed on top of app. - pub modal: Option>>, - /// Whether the arranger is currently focused - pub focused: bool +/// MIDI message serialized to bytes +pub type MIDIMessage = Vec; + +/// Collection of serialized MIDI messages +pub type MIDIChunk = [Vec]; + +/// Contains all phrases in the project +pub struct PhrasePool { + pub phrases: Vec>>>, } -impl Arranger { - pub fn new (name: &str) -> Self { - Self { - name: Arc::new(RwLock::new(name.into())), - mode: ArrangerViewMode::Vertical(2), - selected: ArrangerFocus::Clip(0, 0), - scenes: vec![], - tracks: vec![], - modal: None, - focused: false - } - } - pub fn activate (&mut self) { - match self.selected { - ArrangerFocus::Scene(s) => { - for (track_index, track) in self.tracks.iter_mut().enumerate() { - track.playing_phrase = self.scenes[s].clips[track_index]; - track.reset = true; - } - }, - ArrangerFocus::Clip(t, s) => { - self.tracks[t].playing_phrase = self.scenes[s].clips[t]; - self.tracks[t].reset = true; - }, - _ => {} - } - } - pub fn sequencer (&self) -> Option<&Sequencer> { - self.selected.track() - .map(|track|self.tracks.get(track)) - .flatten() - } - pub fn sequencer_mut (&mut self) -> Option<&mut Sequencer> { - self.selected.track() - .map(|track|self.tracks.get_mut(track)) - .flatten() - } - pub fn show_phrase (&mut self) { - let (scene, track) = (self.selected.scene(), self.selected.track()); - if let (Some(scene_index), Some(track_index)) = (scene, track) { - let scene = self.scenes.get(scene_index); - let track = self.tracks.get_mut(track_index); - if let (Some(scene), Some(track)) = (scene, track) { - track.viewing_phrase = scene.clips[track_index] - } - } - } - pub fn is_first_row (&self) -> bool { - let selected = self.selected; - selected.is_mix() || selected.is_track() - } - pub fn is_last_row (&self) -> bool { - let selected = self.selected; - (self.scenes.len() == 0 && (selected.is_mix() || selected.is_track())) || match selected { - ArrangerFocus::Scene(s) => - s == self.scenes.len() - 1, - ArrangerFocus::Clip(_, s) => - s == self.scenes.len() - 1, - _ => false - } - } - pub fn track (&self) -> Option<&Sequencer> { - self.selected.track().map(|t|self.tracks.get(t)).flatten() - } - pub fn track_mut (&mut self) -> Option<&mut Sequencer> { - 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 Sequencer> { - self.tracks.push(name.map_or_else( - || Sequencer::new(&self.track_default_name()), - |name| Sequencer::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 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) - } - 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_del (&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))) - .map(|(track_index, _)|{ - scene_index - .and_then(|index|self.scenes.get_mut(index)) - .map(|scene|scene.clips[track_index] = None); - }); - } - 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(()) - }) - }); - } +/// Contains state for viewing and editing a phrase +pub struct PhraseEditor { + _engine: PhantomData, + pub phrase: Arc>>, } -/////////////////////////////////////////////////////////////////////////////////////////////////// - -#[derive(PartialEq, Clone, Copy)] -/// 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 description ( - &self, - tracks: &Vec>, - scenes: &Vec, - ) -> String { - format!("Selected: {}", match self { - Self::Mix => format!("Everything"), - Self::Track(t) => if let Some(track) = tracks.get(*t) { - format!("T{t}: {}", &track.name.read().unwrap()) - } else { - format!("T??") - }, - Self::Scene(s) => if let Some(scene) = scenes.get(*s) { - format!("S{s}: {}", &scene.name.read().unwrap()) - } else { - format!("S??") - }, - Self::Clip(t, s) => if let (Some(track), Some(scene)) = ( - tracks.get(*t), - scenes.get(*s), - ) { - if let Some(Some(slot)) = scene.clips.get(*t) { - if let Some(clip) = track.phrases.get(*slot) { - format!("T{t} S{s} C{slot} ({})", &clip.read().unwrap().name.read().unwrap()) - } else { - format!("T{t} S{s}: Empty") - } - } else { - format!("T{t} S{s}: Empty") - } - } else { - format!("T{t} S{s}: Empty") - } - }) - } - pub fn is_mix (&self) -> bool { - match self { Self::Mix => true, _ => false } - } - 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 (&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) - } - } - } - 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)) - } - } - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -/// Display mode of arranger -#[derive(PartialEq)] -pub enum ArrangerViewMode { Horizontal, Vertical(usize) } -/// Arranger display mode can be cycled -impl ArrangerViewMode { - /// Cycle arranger display mode - pub fn to_next (&mut self) { - *self = match self { - Self::Horizontal => Self::Vertical(1), - Self::Vertical(1) => Self::Vertical(2), - Self::Vertical(2) => Self::Vertical(2), - Self::Vertical(0) => Self::Horizontal, - Self::Vertical(_) => Self::Vertical(0), - } - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -pub struct VerticalArranger<'a, E: Engine>( - pub &'a Arranger, pub usize -); -pub struct VerticalArrangerGrid<'a>( - pub u16, pub &'a [(usize, usize)], pub &'a [(usize, usize)] -); -pub struct VerticalArrangerCursor<'a>( - pub bool, pub ArrangerFocus, pub u16, pub &'a [(usize, usize)], pub &'a [(usize, usize)], -); - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -pub struct HorizontalArranger<'a, E: Engine>( - pub &'a Arranger -); - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -/// Appears on first run (i.e. if state dir is missing). -pub struct ArrangerRenameModal { - _engine: std::marker::PhantomData, - pub done: bool, - pub target: ArrangerFocus, - pub value: String, - pub result: Arc>, - pub cursor: usize -} - -impl ArrangerRenameModal { - pub fn new (target: ArrangerFocus, value: &Arc>) -> Self { +impl PhraseEditor { + pub fn new () -> Self { Self { _engine: Default::default(), - done: false, - value: value.read().unwrap().clone(), - cursor: value.read().unwrap().len(), - result: value.clone(), - target, + phrase: Arc::new(RwLock::new(None)), } } + pub fn show (&mut self, phrase: &Arc>>) { + self.phrase = phrase.clone(); + } } -impl Exit for ArrangerRenameModal { - fn exited (&self) -> bool { self.done } - fn exit (&mut self) { self.done = true } +/// A MIDI sequence. +#[derive(Debug)] +pub struct Phrase { + /// Name of phrase + pub name: Arc>, + /// Length of phrase + pub length: usize, + /// Notes in phrase + pub notes: PhraseData, + /// Whether to loop the phrase or play it once + pub loop_on: bool, + /// Start of loop + pub loop_start: usize, + /// Length of loop + pub loop_length: usize, + /// All notes are displayed with minimum length + pub percussive: bool, } -////////////////////////////////////////////////////////////////////////////////////////////////// - -/// A collection of phrases to play on each track. -#[derive(Default)] -pub struct Scene { - pub name: Arc>, - pub clips: Vec>, +impl Default for Phrase { + fn default () -> Self { Self::new("", 0, None) } } -impl Scene { - pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually { - let mut name = None; - let mut clips = vec![]; + +impl Phrase { + pub fn new (name: &str, length: usize, notes: Option) -> Self { + Self { + name: Arc::new(RwLock::new(name.into())), + length, + notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), + loop_on: true, + loop_start: 0, + loop_length: length, + percussive: true, + } + } + pub fn toggle_loop (&mut self) { + self.loop_on = !self.loop_on; + } + 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, + _ => {} + } + } + } + } + pub fn from_edn <'e> (ppq: usize, args: &[Edn<'e>]) -> Usually { + let mut phrase = Self::default(); + let mut name = String::new(); + let mut beats = 0usize; + let mut steps = 0usize; edn!(edn in args { Edn::Map(map) => { - let key = map.get(&Edn::Key(":name")); - if let Some(Edn::Str(n)) = key { - name = Some(*n); - } else { - panic!("unexpected key in scene '{name:?}': {key:?}") + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Int(b)) = map.get(&Edn::Key(":beats")) { + beats = *b as usize; + phrase.length = ppq * beats; + for _ in phrase.notes.len()..phrase.length { + phrase.notes.push(Vec::with_capacity(16)) + } + } + if let Some(Edn::Int(s)) = map.get(&Edn::Key(":steps")) { + steps = *s as usize; } }, - Edn::Symbol("_") => { - clips.push(None); + Edn::List(args) => { + let time = (match args.get(0) { + Some(Edn::Key(text)) => text[1..].parse::()?, + Some(Edn::Int(i)) => *i as f64, + Some(Edn::Double(f)) => f64::from(*f), + _ => panic!("unexpected in phrase '{name}': {:?}", args.get(0)), + } * beats as f64 * ppq as f64 / steps as f64) as usize; + for edn in args[1..].iter() { + match edn { + Edn::List(args) => if let ( + Some(Edn::Int(key)), + Some(Edn::Int(vel)), + ) = ( + args.get(0), + args.get(1), + ) { + let (key, vel) = ( + u7::from((*key as u8).min(127)), + u7::from((*vel as u8).min(127)), + ); + phrase.notes[time].push(MidiMessage::NoteOn { key, vel }) + } else { + panic!("unexpected list in phrase '{name}'") + }, + _ => panic!("unexpected in phrase '{name}': {edn:?}") + } + } }, - Edn::Int(i) => { - clips.push(Some(*i as usize)); - }, - _ => panic!("unexpected in scene '{name:?}': {edn:?}") + _ => panic!("unexpected in phrase '{name}': {edn:?}"), }); - let scene = Self::new(name.unwrap_or(""), clips); - Ok(scene) - } - pub fn new (name: impl AsRef, clips: impl AsRef<[Option]>) -> Self { - let name = Arc::new(RwLock::new(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: &[Sequencer]) -> usize { - self.clips.iter().enumerate() - .filter_map(|(i, c)|c - .map(|c|tracks - .get(i) - .map(|track|track - .phrases - .get(c)))) - .filter_map(|p|p) - .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: &[Sequencer]) -> bool { - self.clips.iter().enumerate() - .all(|(track_index, phrase_index)|match phrase_index { - Some(i) => tracks - .get(track_index) - .map(|track|track.playing_phrase == Some(*i)) - .unwrap_or(false), - None => true - }) + *phrase.name.write().unwrap() = name; + Ok(phrase) } } -/////////////////////////////////////////////////////////////////////////////////////////////////// - -/// Phrase editor. +/// Phrase player. pub struct Sequencer { pub name: Arc>, pub mode: bool, @@ -503,6 +222,7 @@ pub struct Sequencer { /// Highlight keys on piano roll. pub notes_out: [bool;128], } + impl Sequencer { pub fn new (name: &str) -> Self { Self { @@ -654,6 +374,7 @@ impl Sequencer { } } } + /// Add "all notes off" to the start of a buffer. pub fn all_notes_off (output: &mut MIDIChunk) { let mut buf = vec![]; @@ -662,6 +383,7 @@ pub fn all_notes_off (output: &mut MIDIChunk) { evt.write(&mut buf).unwrap(); output[0].push(buf); } + /// Return boxed iterator of MIDI events pub fn parse_midi_input (input: MidiIter) -> Box + '_> { Box::new(input.map(|RawMidi { time, bytes }|( @@ -670,6 +392,7 @@ pub fn parse_midi_input (input: MidiIter) -> Box; -/// Collection of serialized MIDI messages -pub type MIDIChunk = [Vec]; - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -/// A collection of MIDI messages. -pub type PhraseData = Vec>; -/// A MIDI sequence. -#[derive(Debug)] -pub struct Phrase { - pub name: Arc>, - pub length: usize, - pub notes: PhraseData, - pub loop_on: bool, - pub loop_start: usize, - pub loop_length: 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) -> Self { - Self { - name: Arc::new(RwLock::new(name.into())), - length, - notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), - loop_on: true, - loop_start: 0, - loop_length: length, - percussive: true, - } - } - pub fn toggle_loop (&mut self) { - self.loop_on = !self.loop_on; - } - 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, - _ => {} - } - } - } - } - pub fn from_edn <'e> (ppq: usize, args: &[Edn<'e>]) -> Usually { - let mut phrase = Self::default(); - let mut name = String::new(); - let mut beats = 0usize; - let mut steps = 0usize; - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Int(b)) = map.get(&Edn::Key(":beats")) { - beats = *b as usize; - phrase.length = ppq * beats; - for _ in phrase.notes.len()..phrase.length { - phrase.notes.push(Vec::with_capacity(16)) - } - } - if let Some(Edn::Int(s)) = map.get(&Edn::Key(":steps")) { - steps = *s as usize; - } - }, - Edn::List(args) => { - let time = (match args.get(0) { - Some(Edn::Key(text)) => text[1..].parse::()?, - Some(Edn::Int(i)) => *i as f64, - Some(Edn::Double(f)) => f64::from(*f), - _ => panic!("unexpected in phrase '{name}': {:?}", args.get(0)), - } * beats as f64 * ppq as f64 / steps as f64) as usize; - for edn in args[1..].iter() { - match edn { - Edn::List(args) => if let ( - Some(Edn::Int(key)), - Some(Edn::Int(vel)), - ) = ( - args.get(0), - args.get(1), - ) { - let (key, vel) = ( - u7::from((*key as u8).min(127)), - u7::from((*vel as u8).min(127)), - ); - phrase.notes[time].push(MidiMessage::NoteOn { key, vel }) - } else { - panic!("unexpected list in phrase '{name}'") - }, - _ => panic!("unexpected in phrase '{name}': {edn:?}") - } - } - }, - _ => panic!("unexpected in phrase '{name}': {edn:?}"), - }); - *phrase.name.write().unwrap() = name; - Ok(phrase) - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -/// Stores and displays time-related state. -pub struct TransportToolbar { - _engine: PhantomData, - /// Enable metronome? - pub metronome: bool, - /// Current sample rate, tempo, and PPQ. - pub timebase: Arc, - /// JACK client handle (needs to not be dropped for standalone mode to work). - pub jack: Option, - /// JACK transport handle. - pub transport: Option, - /// Quantization factor - /// Global frame and usec at which playback started - pub started: Option<(usize, usize)>, - pub focused: bool, - pub focus: TransportToolbarFocus, - pub playing: Option, - pub bpm: f64, - pub quant: usize, - pub sync: usize, - pub frame: usize, - pub pulse: usize, - pub ppq: usize, - pub usecs: usize, -} - -impl TransportToolbar { - pub fn standalone () -> Usually>> where Self: 'static { - let jack = JackClient::Inactive( - Client::new("tek_transport", ClientOptions::NO_START_SERVER)?.0 - ); - let mut transport = Self::new(Some(jack.transport())); - transport.focused = true; - let transport = Arc::new(RwLock::new(transport)); - transport.write().unwrap().jack = Some( - jack.activate( - &transport.clone(), - |state: &Arc>>, client, scope| { - state.write().unwrap().process(client, scope) - } - )? - ); - Ok(transport) - } - pub fn new (transport: Option) -> Self { - let timebase = Arc::new(Timebase::default()); - Self { - _engine: Default::default(), - focused: false, - focus: TransportToolbarFocus::PlayPause, - playing: Some(TransportState::Stopped), - bpm: timebase.bpm(), - quant: 24, - sync: timebase.ppq() as usize * 4, - frame: 0, - pulse: 0, - ppq: 0, - usecs: 0, - metronome: false, - started: None, - jack: None, - transport, - timebase, - } - } - pub fn toggle_play (&mut self) -> Usually<()> { - let transport = self.transport.as_ref().unwrap(); - self.playing = match self.playing.expect("1st frame has not been processed yet") { - TransportState::Stopped => { - transport.start()?; - Some(TransportState::Starting) - }, - _ => { - transport.stop()?; - transport.locate(0)?; - Some(TransportState::Stopped) - }, - }; - Ok(()) - } - pub fn update (&mut self, scope: &ProcessScope) -> (bool, usize, usize, usize, usize, f64) { - let times = scope.cycle_times().unwrap(); - let CycleTimes { current_frames, current_usecs, next_usecs, period_usecs } = times; - let chunk_size = scope.n_frames() as usize; - let transport = self.transport.as_ref().unwrap().query().unwrap(); - self.frame = transport.pos.frame() as usize; - let mut reset = false; - if self.playing != Some(transport.state) { - match transport.state { - TransportState::Rolling => { - self.started = Some((current_frames as usize, current_usecs as usize)); - }, - TransportState::Stopped => { - self.started = None; - reset = true; - }, - _ => {} - } - } - self.playing = Some(transport.state); - ( - reset, - current_frames as usize, - chunk_size as usize, - current_usecs as usize, - next_usecs as usize, - period_usecs as f64 - ) - } - pub fn bpm (&self) -> usize { - self.timebase.bpm() as usize - } - pub fn ppq (&self) -> usize { - self.timebase.ppq() as usize - } - pub fn pulse (&self) -> usize { - self.timebase.frame_to_pulse(self.frame as f64) as usize - } - pub fn usecs (&self) -> usize { - self.timebase.frame_to_usec(self.frame as f64) as usize - } - pub fn quant (&self) -> usize { - self.quant - } - pub fn sync (&self) -> usize { - self.sync - } -} - -impl Audio for TransportToolbar { - fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - self.update(&scope); - Control::Continue - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -#[derive(Clone, Copy, PartialEq)] -pub enum TransportToolbarFocus { PlayPause, Bpm, Quant, Sync, Clock, } -impl TransportToolbarFocus { - pub fn next (&mut self) { - *self = match self { - Self::PlayPause => Self::Bpm, - Self::Bpm => Self::Quant, - Self::Quant => Self::Sync, - Self::Sync => Self::Clock, - Self::Clock => Self::PlayPause, - } - } - pub fn prev (&mut self) { - *self = match self { - Self::PlayPause => Self::Clock, - Self::Bpm => Self::PlayPause, - Self::Quant => Self::Bpm, - Self::Sync => Self::Quant, - Self::Clock => Self::Sync, - } - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/crates/tek_sequencer/src/sequencer_tui.rs b/crates/tek_sequencer/src/sequencer_tui.rs index 78db48d8..94802c16 100644 --- a/crates/tek_sequencer/src/sequencer_tui.rs +++ b/crates/tek_sequencer/src/sequencer_tui.rs @@ -1,658 +1,5 @@ use crate::*; -/////////////////////////////////////////////////////////////////////////////////////////////////// - -const CORNERS: CornersTall = CornersTall(NOT_DIM_GREEN); - -tui_style!(GRAY_DIM = - Some(Color::Gray), None, None, Modifier::DIM, Modifier::empty()); -tui_style!(GRAY_NOT_DIM_BOLD = - Some(Color::Gray), None, None, Modifier::BOLD, Modifier::DIM); -tui_style!(NOT_DIM_BOLD = - None, None, None, Modifier::BOLD, Modifier::DIM); -tui_style!(NOT_DIM_GREEN = - Some(Color::Rgb(96, 255, 32)), Some(COLOR_BG1), None, Modifier::empty(), Modifier::DIM); -tui_style!(NOT_DIM = - None, None, None, Modifier::empty(), Modifier::DIM); -tui_style!(WHITE_NOT_DIM_BOLD = - Some(Color::White), None, None, Modifier::BOLD, Modifier::DIM); -tui_style!(STYLE_LABEL = - Some(Color::Reset), None, None, Modifier::empty(), Modifier::BOLD); -tui_style!(STYLE_VALUE = - Some(Color::White), None, None, Modifier::BOLD, Modifier::DIM); - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -impl Arranger { - pub fn rename_selected (&mut self) { - self.modal = Some(Box::new(ArrangerRenameModal::new( - self.selected, - &match self.selected { - ArrangerFocus::Mix => self.name.clone(), - ArrangerFocus::Track(t) => self.tracks[t].name.clone(), - ArrangerFocus::Scene(s) => self.scenes[s].name.clone(), - ArrangerFocus::Clip(t, s) => self.tracks[t].phrases[s].read().unwrap().name.clone(), - } - ))); - } -} -impl Focusable for Arranger { - fn is_focused (&self) -> bool { - self.focused - } - fn set_focused (&mut self, focused: bool) { - self.focused = focused - } -} -impl Handle for Arranger { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - if let Some(modal) = self.modal.as_mut() { - let result = modal.handle(from)?; - if from.is_done() { - self.modal = None; - } - return Ok(result) - } - match from.event() { - // mode_switch: switch the display mode - key!(KeyCode::Char('`')) => { - self.mode.to_next() - }, - // cursor_up: move cursor up - key!(KeyCode::Up) => { - match self.mode { - ArrangerViewMode::Horizontal => self.track_prev(), - _ => self.scene_prev(), - }; - self.show_phrase(); - }, - // cursor_down - key!(KeyCode::Down) => { - match self.mode { - ArrangerViewMode::Horizontal => self.track_next(), - _ => self.scene_next(), - }; - self.show_phrase(); - }, - // cursor left - key!(KeyCode::Left) => { - match self.mode { - ArrangerViewMode::Horizontal => self.scene_prev(), - _ => self.track_prev(), - }; - self.show_phrase(); - }, - // cursor right - key!(KeyCode::Right) => { - match self.mode { - ArrangerViewMode::Horizontal => self.scene_next(), - _ => self.track_next(), - }; - self.show_phrase(); - }, - // increment: remove clip - key!(KeyCode::Delete) => { - self.phrase_del(); - self.show_phrase(); - }, - // increment: use next clip here - key!(KeyCode::Char('.')) => { - self.phrase_next(); - self.show_phrase(); - }, - // decrement: use previous next clip here - key!(KeyCode::Char(',')) => { - self.phrase_prev(); - self.show_phrase(); - }, - // decrement: use previous clip here - key!(KeyCode::Enter) => { - self.activate(); - }, - // scene_add: add a new scene - key!(Ctrl-KeyCode::Char('a')) => { - self.scene_add(None)?; - }, - // track_add: add a new scene - key!(Ctrl-KeyCode::Char('t')) => { - self.track_add(None)?; - }, - // rename: add a new scene - key!(KeyCode::Char('n')) => { - self.rename_selected(); - }, - // length: add a new scene - key!(KeyCode::Char('l')) => if let Some(phrase) = self.phrase() { - phrase.write().unwrap().toggle_loop() - }, - // color: set color of item at cursor - key!(KeyCode::Char('c')) => { - todo!(); - }, - _ => return Ok(None) - } - Ok(Some(true)) - } -} -impl Content for Arranger { - type Engine = Tui; - fn content (&self) -> impl Widget { - Layers::new(move |add|{ - match self.mode { - ArrangerViewMode::Horizontal => add(&HorizontalArranger(&self)), - ArrangerViewMode::Vertical(factor) => add(&VerticalArranger(&self, factor)) - }?; - add(&Align::SE(self.selected.description( - &self.tracks, - &self.scenes, - ).as_str())) - }) - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -impl<'a> Content for VerticalArranger<'a, Tui> { - type Engine = Tui; - fn content (&self) -> impl Widget { - let Self(state, factor) = self; - let ppq = 96; - let (cols, rows) = if *factor == 0 {( - track_clip_name_lengths(state.tracks.as_slice()), - scene_ppqs(state.tracks.as_slice(), state.scenes.as_slice()), - )} else {( - track_clip_name_lengths(state.tracks.as_slice()), - (0..=state.scenes.len()).map(|i|(factor*ppq, factor*ppq*i)).collect::>(), - )}; - //let height = rows.last().map(|(w,y)|(y+w)/ppq).unwrap_or(16); - let tracks: &[Sequencer] = state.tracks.as_ref(); - let scenes: &[Scene] = state.scenes.as_ref(); - let offset = 4 + scene_name_max_len(scenes) as u16; - Layers::new(move |add|{ - let rows: &[(usize, usize)] = rows.as_ref(); - let cols: &[(usize, usize)] = cols.as_ref(); - - let track_titles = row!((track, (w, _)) in tracks.iter().zip(cols) => - (&track.name.read().unwrap().as_str() as &dyn Widget) - .min_xy(*w as u16, 2).push_x(offset)); - - let scene_name = |scene, playing: bool, height|row!( - if playing { "▶ " } else { " " }, - (scene as &Scene).name.read().unwrap().as_str(), - ).fixed_xy(offset.saturating_sub(1), height); - - let scene_clip = |scene, track: usize, w: u16, h: u16|Layers::new(move |add|{ - let mut color = Color::Rgb(40, 50, 30); - match (tracks.get(track), (scene as &Scene).clips.get(track)) { - (Some(track), Some(Some(clip))) => match track.phrases.get(*clip) { - Some(phrase) => { - let name = &(phrase as &Arc>).read().unwrap().name; - let name = name.read().unwrap(); - let name = format!("{clip:02} {}", name); - add(&name.as_str().push_x(1).fixed_x(w))?; - if (track as &Sequencer<_>).playing_phrase == Some(*clip) { - color = COLOR_PLAYING - } else { - color = COLOR_BG1 - }; - }, - _ => {} - }, - _ => {} - }; - add(&Background(color)) - }).fixed_xy(w, h); - - let tracks_clips = col!((scene, (pulses, _)) in scenes.iter().zip(rows) => { - let height = 1.max((pulses / 96) as u16); - let playing = scene.is_playing(tracks); - Stack::right(move |add| { - add(&scene_name(scene, playing, height))?; - for (track, (w, _x)) in cols.iter().enumerate() { - add(&scene_clip(scene, track, *w as u16, height))?; - } - Ok(()) - }).fixed_y(height) - }); - - add(&VerticalArrangerGrid(offset, &rows, &cols))?; - add(&VerticalArrangerCursor(state.focused, state.selected, offset, &cols, &rows))?; - add(&col!(track_titles, tracks_clips))?; - Ok(()) - }) - .bg(Color::Rgb(28, 35, 25)) - .border(Lozenge(Style::default() - .bg(Color::Rgb(40, 50, 30)) - .fg(Color::Rgb(70, 80, 50)))) - } -} - -pub fn track_clip_name_lengths (tracks: &[Sequencer]) -> Vec<(usize, usize)> { - let mut total = 0; - let mut lengths: Vec<(usize, usize)> = tracks.iter().map(|track|{ - let len = 4 + track.phrases - .iter() - .fold(track.name.read().unwrap().len(), |len, phrase|{ - len.max(phrase.read().unwrap().name.read().unwrap().len()) - }); - total = total + len; - (len, total - len) - }).collect(); - lengths.push((0, total)); - lengths -} - -pub fn scene_ppqs (tracks: &[Sequencer], scenes: &[Scene]) -> Vec<(usize, usize)> { - let mut total = 0; - let mut scenes: Vec<(usize, usize)> = scenes.iter().map(|scene|{ - let pulses = scene.pulses(tracks).max(96); - total = total + pulses; - (pulses, total - pulses) - }).collect(); - scenes.push((0, total)); - scenes -} - -pub fn scene_name_max_len (scenes: &[Scene]) -> usize { - scenes.iter() - .map(|s|s.name.read().unwrap().len()) - .fold(0, usize::max) -} - -impl<'a> Widget for VerticalArrangerGrid<'a> { - type Engine = Tui; - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - let area = to.area(); - let Self(offset, rows, cols) = self; - let style = Some(Style::default().fg(COLOR_SEPARATOR)); - for (_, x) in cols.iter() { - let x = offset + area.x() + *x as u16 - 1; - for y in area.y()..area.y2() { - to.blit(&"▎", x, y, style); - } - } - for (_, y) in rows.iter() { - let y = area.y() + (*y / 96) as u16 + 1; - if y >= to.buffer.area.height { - break - } - for x in area.x()..area.x2().saturating_sub(2) { - if x < to.buffer.area.x && y < to.buffer.area.y { - let cell = to.buffer.get_mut(x, y); - cell.modifier = Modifier::UNDERLINED; - cell.underline_color = COLOR_SEPARATOR; - } - } - } - Ok(()) - } -} - -impl<'a> Widget for VerticalArrangerCursor<'a> { - type Engine = Tui; - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - let area = to.area(); - let Self(focused, selected, offset, cols, rows) = *self; - let get_track_area = |t: usize| [ - offset + area.x() + cols[t].1 as u16 - 1, - area.y(), - cols[t].0 as u16, - area.h() - ]; - let get_scene_area = |s: usize| [ - area.x(), - 2 + area.y() + (rows[s].1 / 96) as u16, - area.w(), - (rows[s].0 / 96) as u16 - ]; - let get_clip_area = |t: usize, s: usize| [ - offset + area.x() + cols[t].1 as u16 - 1, - 2 + area.y() + (rows[s].1 / 96) as u16, - cols[t].0 as u16, - (rows[s].0 / 96) as u16 - ]; - let mut track_area: Option<[u16;4]> = None; - let mut scene_area: Option<[u16;4]> = None; - let mut clip_area: Option<[u16;4]> = None; - let area = match selected { - ArrangerFocus::Mix => { - if focused { - to.fill_bg(area, Color::Rgb(40, 50, 30)); - } - area - }, - ArrangerFocus::Track(t) => { - track_area = Some(get_track_area(t)); - area - }, - ArrangerFocus::Scene(s) => { - scene_area = Some(get_scene_area(s)); - area - }, - ArrangerFocus::Clip(t, s) => { - track_area = Some(get_track_area(t)); - scene_area = Some(get_scene_area(s)); - clip_area = Some(get_clip_area(t, s)); - area - }, - }; - if let Some([x, y, width, height]) = track_area { - to.fill_fg([x, y, 1, height], Color::Rgb(70, 80, 50)); - to.fill_fg([x + width, y, 1, height], Color::Rgb(70, 80, 50)); - } - if let Some([_, y, _, height]) = scene_area { - to.fill_ul([area.x(), y - 1, area.w(), 1], Color::Rgb(70, 80, 50)); - to.fill_ul([area.x(), y + height - 1, area.w(), 1], Color::Rgb(70, 80, 50)); - } - if focused { - if let Some(clip_area) = clip_area { - to.render_in(clip_area, &CORNERS)?; - to.fill_bg(clip_area, Color::Rgb(40, 50, 30)); - } else if let Some(track_area) = track_area { - to.render_in(track_area.clip_h(2), &CORNERS)?; - to.fill_bg(track_area, Color::Rgb(40, 50, 30)); - } else if let Some(scene_area) = scene_area { - to.render_in(scene_area.clip_w(offset-1), &CORNERS)?; - to.fill_bg(scene_area, Color::Rgb(40, 50, 30)); - } - } - Ok(()) - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -impl<'a> Content for HorizontalArranger<'a, Tui> { - type Engine = Tui; - fn content (&self) -> impl Widget { - let Arranger { tracks, focused, selected, scenes, .. } = self.0; - let _tracks = tracks.as_slice(); - lay!( - focused.then_some(Background(Color::Rgb(40, 50, 30))), - row!( - // name - CustomWidget::new(|_|{ - todo!() - }, |_: &mut TuiOutput|{ - todo!() - //let Self(tracks, selected) = self; - //let yellow = Some(Style::default().yellow().bold().not_dim()); - //let white = Some(Style::default().white().bold().not_dim()); - //let area = to.area(); - //let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()]; - //let offset = 0; // track scroll offset - //for y in 0..area.h() { - //if y == 0 { - //to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?; - //} else if y % 2 == 0 { - //let index = (y as usize - 2) / 2 + offset; - //if let Some(track) = tracks.get(index) { - //let selected = selected.track() == Some(index); - //let style = if selected { yellow } else { white }; - //to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?; - //to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?; - //} - //} - //} - //Ok(Some(area)) - }), - // monitor - CustomWidget::new(|_|{ - todo!() - }, |_: &mut TuiOutput|{ - todo!() - //let Self(tracks) = self; - //let mut area = to.area(); - //let on = Some(Style::default().not_dim().green().bold()); - //let off = Some(DIM); - //area.x += 1; - //for y in 0..area.h() { - //if y == 0 { - ////" MON ".blit(to.buffer, area.x, area.y + y, style2)?; - //} else if y % 2 == 0 { - //let index = (y as usize - 2) / 2; - //if let Some(track) = tracks.get(index) { - //let style = if track.monitoring { on } else { off }; - //to.blit(&" MON ", area.x(), area.y() + y, style)?; - //} else { - //area.height = y; - //break - //} - //} - //} - //area.width = 4; - //Ok(Some(area)) - }), - // record - CustomWidget::new(|_|{ - todo!() - }, |_: &mut TuiOutput|{ - todo!() - //let Self(tracks) = self; - //let mut area = to.area(); - //let on = Some(Style::default().not_dim().red().bold()); - //let off = Some(Style::default().dim()); - //area.x += 1; - //for y in 0..area.h() { - //if y == 0 { - ////" REC ".blit(to.buffer, area.x, area.y + y, style2)?; - //} else if y % 2 == 0 { - //let index = (y as usize - 2) / 2; - //if let Some(track) = tracks.get(index) { - //let style = if track.recording { on } else { off }; - //to.blit(&" REC ", area.x(), area.y() + y, style)?; - //} else { - //area.height = y; - //break - //} - //} - //} - //area.width = 4; - //Ok(Some(area)) - }), - // overdub - CustomWidget::new(|_|{ - todo!() - }, |_: &mut TuiOutput|{ - todo!() - //let Self(tracks) = self; - //let mut area = to.area(); - //let on = Some(Style::default().not_dim().yellow().bold()); - //let off = Some(Style::default().dim()); - //area.x = area.x + 1; - //for y in 0..area.h() { - //if y == 0 { - ////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?; - //} else if y % 2 == 0 { - //let index = (y as usize - 2) / 2; - //if let Some(track) = tracks.get(index) { - //to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub { - //on - //} else { - //off - //})?; - //} else { - //area.height = y; - //break - //} - //} - //} - //area.width = 4; - //Ok(Some(area)) - }), - // erase - CustomWidget::new(|_|{ - todo!() - }, |_: &mut TuiOutput|{ - todo!() - //let Self(tracks) = self; - //let mut area = to.area(); - //let off = Some(Style::default().dim()); - //area.x = area.x + 1; - //for y in 0..area.h() { - //if y == 0 { - ////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?; - //} else if y % 2 == 0 { - //let index = (y as usize - 2) / 2; - //if let Some(_) = tracks.get(index) { - //to.blit(&" DEL ", area.x(), area.y() + y, off)?; - //} else { - //area.height = y; - //break - //} - //} - //} - //area.width = 4; - //Ok(Some(area)) - }), - // gain - CustomWidget::new(|_|{ - todo!() - }, |_: &mut TuiOutput|{ - todo!() - //let Self(tracks) = self; - //let mut area = to.area(); - //let off = Some(Style::default().dim()); - //area.x = area.x() + 1; - //for y in 0..area.h() { - //if y == 0 { - ////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?; - //} else if y % 2 == 0 { - //let index = (y as usize - 2) / 2; - //if let Some(_) = tracks.get(index) { - //to.blit(&" +0.0 ", area.x(), area.y() + y, off)?; - //} else { - //area.height = y; - //break - //} - //} - //} - //area.width = 7; - //Ok(Some(area)) - }), - // scenes - CustomWidget::new(|_|{ - todo!() - }, |to: &mut TuiOutput|{ - let Arranger { tracks, scenes, selected, .. } = self.0; - let area = to.area(); - let mut x2 = 0; - let [x, y, _, height] = area; - for (scene_index, scene) in scenes.iter().enumerate() { - let active_scene = 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 { - to.blit(&"│", x + x2, y, sep); - } - let name = scene.name.read().unwrap(); - let mut x3 = name.len() as u16; - to.blit(&*name, x + x2, y, sep); - for (i, clip) in scene.clips.iter().enumerate() { - let active_track = selected.track() == Some(i); - if let Some(clip) = clip { - let y2 = y + 2 + i as u16 * 2; - let label = match tracks[i].phrases.get(*clip) { - Some(phrase) => &format!("{}", phrase.read().unwrap().name.read().unwrap()), - None => "...." - }; - to.blit(&label, 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(Some([x, y, x2, height])) - Ok(()) - }), - ) - ) - } -} - -pub fn track_name_max_len (tracks: &[Sequencer]) -> usize { - tracks.iter() - .map(|s|s.name.read().unwrap().len()) - .fold(0, usize::max) -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -impl Content for ArrangerRenameModal { - type Engine = Tui; - fn content (&self) -> impl Widget { - todo!(); - Layers::new(|add|{Ok(())}) - //let area = to.area(); - //let y = area.y() + area.h() / 2; - //let bg_area = [1, y - 1, area.w() - 2, 3]; - //to.fill_bg(bg_area, COLOR_BG1); - //Lozenge(Style::default().bold().white().dim()).draw(to.with_rect(bg_area)); - //let label = match self.target { - //ArrangerFocus::Mix => "Rename project:", - //ArrangerFocus::Track(_) => "Rename track:", - //ArrangerFocus::Scene(_) => "Rename scene:", - //ArrangerFocus::Clip(_, _) => "Rename clip:", - //}; - //let style = Some(Style::default().not_bold().white().not_dim()); - //to.blit(&label, area.x() + 3, y, style); - //let style = Some(Style::default().bold().white().not_dim()); - //to.blit(&self.value, area.x() + 3 + label.len() as u16 + 1, y, style); - //let style = Some(Style::default().bold().white().not_dim().reversed()); - //to.blit(&"▂", area.x() + 3 + label.len() as u16 + 1 + self.cursor as u16, y, style); - //Ok(Some(area)) - //Ok(()) - } -} - -impl Handle for ArrangerRenameModal { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - match from.event() { - TuiEvent::Input(Event::Key(k)) => { - match k.code { - KeyCode::Esc => { - self.exit(); - }, - KeyCode::Enter => { - *self.result.write().unwrap() = self.value.clone(); - self.exit(); - }, - KeyCode::Left => { - self.cursor = self.cursor.saturating_sub(1); - }, - KeyCode::Right => { - self.cursor = self.value.len().min(self.cursor + 1) - }, - KeyCode::Backspace => { - let last = self.value.len().saturating_sub(1); - self.value = format!("{}{}", - &self.value[0..self.cursor.min(last)], - &self.value[self.cursor.min(last)..last] - ); - self.cursor = self.cursor.saturating_sub(1) - } - KeyCode::Char(c) => { - self.value.insert(self.cursor, c); - self.cursor = self.value.len().min(self.cursor + 1) - }, - _ => {} - } - Ok(Some(true)) - }, - _ => Ok(None), - } - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - impl Sequencer { const H_KEYS_OFFSET: usize = 5; /// Select which pattern to display. This pre-renders it to the buffer at full resolution. @@ -954,125 +301,3 @@ impl Handle for Sequencer { return Ok(Some(true)) } } - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -impl TransportToolbar { - fn handle_bpm (&mut self, from: &TuiInput) -> Perhaps { - match from.event() { - key!(KeyCode::Char(',')) => { self.bpm -= 1.0; }, - key!(KeyCode::Char('.')) => { self.bpm += 1.0; }, - key!(KeyCode::Char('<')) => { self.bpm -= 0.001; }, - key!(KeyCode::Char('>')) => { self.bpm += 0.001; }, - _ => return Ok(None) - } - Ok(Some(true)) - } - fn handle_quant (&mut self, from: &TuiInput) -> Perhaps { - match from.event() { - key!(KeyCode::Char(',')) => { - self.quant = prev_note_length(self.quant); - Ok(Some(true)) - }, - key!(KeyCode::Char('.')) => { - self.quant = next_note_length(self.quant); - Ok(Some(true)) - }, - _ => Ok(None) - } - } - fn handle_sync (&mut self, from: &TuiInput) -> Perhaps { - match from.event() { - key!(KeyCode::Char(',')) => { - self.sync = prev_note_length(self.sync); - Ok(Some(true)) - }, - key!(KeyCode::Char('.')) => { - self.sync = next_note_length(self.sync); - Ok(Some(true)) - }, - _ => Ok(None) - } - } -} - -impl Handle for TransportToolbar { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - match from.event() { - key!(KeyCode::Left) => { self.focus.prev(); }, - key!(KeyCode::Right) => { self.focus.next(); }, - _ => match self.focus { - TransportToolbarFocus::PlayPause => self.toggle_play().map(|_|())?, - TransportToolbarFocus::Bpm => self.handle_bpm(from).map(|_|())?, - TransportToolbarFocus::Quant => self.handle_quant(from).map(|_|())?, - TransportToolbarFocus::Sync => self.handle_sync(from).map(|_|())?, - TransportToolbarFocus::Clock => {/*todo*/}, - } - } - Ok(Some(true)) - } -} - -impl Content for TransportToolbar { - type Engine = Tui; - fn content (&self) -> impl Widget { - row!( - self.focus.wrap(self.focused, TransportToolbarFocus::PlayPause, &Styled( - match self.playing { - Some(TransportState::Stopped) => Some(GRAY_DIM.bold()), - Some(TransportState::Starting) => Some(GRAY_NOT_DIM_BOLD), - Some(TransportState::Rolling) => Some(WHITE_NOT_DIM_BOLD), - _ => unreachable!(), - }, - match self.playing { - Some(TransportState::Rolling) => "▶ PLAYING", - Some(TransportState::Starting) => "READY ...", - Some(TransportState::Stopped) => "⏹ STOPPED", - _ => unreachable!(), - } - ).min_xy(11, 2).push_x(1)), - self.focus.wrap(self.focused, TransportToolbarFocus::Bpm, &Outset::X(1u16, col! { - "BPM", format!("{}.{:03}", self.bpm as usize, (self.bpm * 1000.0) % 1000.0) - })), - self.focus.wrap(self.focused, TransportToolbarFocus::Quant, &Outset::X(1u16, col! { - "QUANT", ppq_to_name(self.quant as usize) - })), - self.focus.wrap(self.focused, TransportToolbarFocus::Sync, &Outset::X(1u16, col! { - "SYNC", ppq_to_name(self.sync as usize) - })), - self.focus.wrap(self.focused, TransportToolbarFocus::Clock, &{ - let Self { frame: _frame, pulse, ppq, usecs, .. } = self; - let (beats, pulses) = if *ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - let (bars, beats) = ((beats / 4) + 1, (beats % 4) + 1); - let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); - let (minutes, seconds) = (seconds / 60, seconds % 60); - let time1 = format!("{bars}.{beats}.{pulses:02}"); - let time2 = format!("{minutes}:{seconds:02}:{msecs:03}"); - col!(time1.as_str(), time2.as_str()).outset_x(1) - }), - ).fill_x().bg(Color::Rgb(40, 50, 30)) - } -} - -impl Focusable for TransportToolbar { - fn is_focused (&self) -> bool { - self.focused - } - fn set_focused (&mut self, focused: bool) { - self.focused = focused - } -} - -impl TransportToolbarFocus { - pub fn wrap <'a, W: Widget> ( - self, parent_focus: bool, focus: Self, widget: &'a W - ) -> impl Widget + 'a { - Layers::new(move |add|{ - if parent_focus && focus == self { - add(&CORNERS)?; - add(&Background(Color::Rgb(60, 70, 50)))?; - } - add(widget) - }) - } -} diff --git a/crates/tek_sequencer/src/transport.rs b/crates/tek_sequencer/src/transport.rs new file mode 100644 index 00000000..1870bd39 --- /dev/null +++ b/crates/tek_sequencer/src/transport.rs @@ -0,0 +1,162 @@ +use crate::*; + +/// Stores and displays time-related state. +pub struct TransportToolbar { + _engine: PhantomData, + /// Enable metronome? + pub metronome: bool, + /// Current sample rate, tempo, and PPQ. + pub timebase: Arc, + /// JACK client handle (needs to not be dropped for standalone mode to work). + pub jack: Option, + /// JACK transport handle. + pub transport: Option, + /// Quantization factor + /// Global frame and usec at which playback started + pub started: Option<(usize, usize)>, + pub focused: bool, + pub focus: TransportToolbarFocus, + pub playing: Option, + pub bpm: f64, + pub quant: usize, + pub sync: usize, + pub frame: usize, + pub pulse: usize, + pub ppq: usize, + pub usecs: usize, +} + +impl TransportToolbar { + pub fn standalone () -> Usually>> where Self: 'static { + let jack = JackClient::Inactive( + Client::new("tek_transport", ClientOptions::NO_START_SERVER)?.0 + ); + let mut transport = Self::new(Some(jack.transport())); + transport.focused = true; + let transport = Arc::new(RwLock::new(transport)); + transport.write().unwrap().jack = Some( + jack.activate( + &transport.clone(), + |state: &Arc>>, client, scope| { + state.write().unwrap().process(client, scope) + } + )? + ); + Ok(transport) + } + pub fn new (transport: Option) -> Self { + let timebase = Arc::new(Timebase::default()); + Self { + _engine: Default::default(), + focused: false, + focus: TransportToolbarFocus::PlayPause, + playing: Some(TransportState::Stopped), + bpm: timebase.bpm(), + quant: 24, + sync: timebase.ppq() as usize * 4, + frame: 0, + pulse: 0, + ppq: 0, + usecs: 0, + metronome: false, + started: None, + jack: None, + transport, + timebase, + } + } + pub fn toggle_play (&mut self) -> Usually<()> { + let transport = self.transport.as_ref().unwrap(); + self.playing = match self.playing.expect("1st frame has not been processed yet") { + TransportState::Stopped => { + transport.start()?; + Some(TransportState::Starting) + }, + _ => { + transport.stop()?; + transport.locate(0)?; + Some(TransportState::Stopped) + }, + }; + Ok(()) + } + pub fn update (&mut self, scope: &ProcessScope) -> (bool, usize, usize, usize, usize, f64) { + let times = scope.cycle_times().unwrap(); + let CycleTimes { current_frames, current_usecs, next_usecs, period_usecs } = times; + let chunk_size = scope.n_frames() as usize; + let transport = self.transport.as_ref().unwrap().query().unwrap(); + self.frame = transport.pos.frame() as usize; + let mut reset = false; + if self.playing != Some(transport.state) { + match transport.state { + TransportState::Rolling => { + self.started = Some((current_frames as usize, current_usecs as usize)); + }, + TransportState::Stopped => { + self.started = None; + reset = true; + }, + _ => {} + } + } + self.playing = Some(transport.state); + ( + reset, + current_frames as usize, + chunk_size as usize, + current_usecs as usize, + next_usecs as usize, + period_usecs as f64 + ) + } + pub fn bpm (&self) -> usize { + self.timebase.bpm() as usize + } + pub fn ppq (&self) -> usize { + self.timebase.ppq() as usize + } + pub fn pulse (&self) -> usize { + self.timebase.frame_to_pulse(self.frame as f64) as usize + } + pub fn usecs (&self) -> usize { + self.timebase.frame_to_usec(self.frame as f64) as usize + } + pub fn quant (&self) -> usize { + self.quant + } + pub fn sync (&self) -> usize { + self.sync + } +} + +impl Audio for TransportToolbar { + fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + self.update(&scope); + Control::Continue + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#[derive(Clone, Copy, PartialEq)] +pub enum TransportToolbarFocus { PlayPause, Bpm, Quant, Sync, Clock, } +impl TransportToolbarFocus { + pub fn next (&mut self) { + *self = match self { + Self::PlayPause => Self::Bpm, + Self::Bpm => Self::Quant, + Self::Quant => Self::Sync, + Self::Sync => Self::Clock, + Self::Clock => Self::PlayPause, + } + } + pub fn prev (&mut self) { + *self = match self { + Self::PlayPause => Self::Clock, + Self::Bpm => Self::PlayPause, + Self::Quant => Self::Bpm, + Self::Sync => Self::Quant, + Self::Clock => Self::Sync, + } + } +} diff --git a/crates/tek_sequencer/src/transport_tui.rs b/crates/tek_sequencer/src/transport_tui.rs new file mode 100644 index 00000000..2be49ee7 --- /dev/null +++ b/crates/tek_sequencer/src/transport_tui.rs @@ -0,0 +1,121 @@ +use crate::*; + +impl TransportToolbar { + fn handle_bpm (&mut self, from: &TuiInput) -> Perhaps { + match from.event() { + key!(KeyCode::Char(',')) => { self.bpm -= 1.0; }, + key!(KeyCode::Char('.')) => { self.bpm += 1.0; }, + key!(KeyCode::Char('<')) => { self.bpm -= 0.001; }, + key!(KeyCode::Char('>')) => { self.bpm += 0.001; }, + _ => return Ok(None) + } + Ok(Some(true)) + } + fn handle_quant (&mut self, from: &TuiInput) -> Perhaps { + match from.event() { + key!(KeyCode::Char(',')) => { + self.quant = prev_note_length(self.quant); + Ok(Some(true)) + }, + key!(KeyCode::Char('.')) => { + self.quant = next_note_length(self.quant); + Ok(Some(true)) + }, + _ => Ok(None) + } + } + fn handle_sync (&mut self, from: &TuiInput) -> Perhaps { + match from.event() { + key!(KeyCode::Char(',')) => { + self.sync = prev_note_length(self.sync); + Ok(Some(true)) + }, + key!(KeyCode::Char('.')) => { + self.sync = next_note_length(self.sync); + Ok(Some(true)) + }, + _ => Ok(None) + } + } +} + +impl Handle for TransportToolbar { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + match from.event() { + key!(KeyCode::Left) => { self.focus.prev(); }, + key!(KeyCode::Right) => { self.focus.next(); }, + _ => match self.focus { + TransportToolbarFocus::PlayPause => self.toggle_play().map(|_|())?, + TransportToolbarFocus::Bpm => self.handle_bpm(from).map(|_|())?, + TransportToolbarFocus::Quant => self.handle_quant(from).map(|_|())?, + TransportToolbarFocus::Sync => self.handle_sync(from).map(|_|())?, + TransportToolbarFocus::Clock => {/*todo*/}, + } + } + Ok(Some(true)) + } +} + +impl Content for TransportToolbar { + type Engine = Tui; + fn content (&self) -> impl Widget { + row!( + self.focus.wrap(self.focused, TransportToolbarFocus::PlayPause, &Styled( + match self.playing { + Some(TransportState::Stopped) => Some(GRAY_DIM.bold()), + Some(TransportState::Starting) => Some(GRAY_NOT_DIM_BOLD), + Some(TransportState::Rolling) => Some(WHITE_NOT_DIM_BOLD), + _ => unreachable!(), + }, + match self.playing { + Some(TransportState::Rolling) => "▶ PLAYING", + Some(TransportState::Starting) => "READY ...", + Some(TransportState::Stopped) => "⏹ STOPPED", + _ => unreachable!(), + } + ).min_xy(11, 2).push_x(1)), + self.focus.wrap(self.focused, TransportToolbarFocus::Bpm, &Outset::X(1u16, col! { + "BPM", format!("{}.{:03}", self.bpm as usize, (self.bpm * 1000.0) % 1000.0) + })), + self.focus.wrap(self.focused, TransportToolbarFocus::Quant, &Outset::X(1u16, col! { + "QUANT", ppq_to_name(self.quant as usize) + })), + self.focus.wrap(self.focused, TransportToolbarFocus::Sync, &Outset::X(1u16, col! { + "SYNC", ppq_to_name(self.sync as usize) + })), + self.focus.wrap(self.focused, TransportToolbarFocus::Clock, &{ + let Self { frame: _frame, pulse, ppq, usecs, .. } = self; + let (beats, pulses) = if *ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + let (bars, beats) = ((beats / 4) + 1, (beats % 4) + 1); + let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); + let (minutes, seconds) = (seconds / 60, seconds % 60); + let time1 = format!("{bars}.{beats}.{pulses:02}"); + let time2 = format!("{minutes}:{seconds:02}:{msecs:03}"); + col!(time1.as_str(), time2.as_str()).outset_x(1) + }), + ).fill_x().bg(Color::Rgb(40, 50, 30)) + } +} + +impl Focusable for TransportToolbar { + fn is_focused (&self) -> bool { + self.focused + } + fn set_focused (&mut self, focused: bool) { + self.focused = focused + } +} + +impl TransportToolbarFocus { + pub fn wrap <'a, W: Widget> ( + self, parent_focus: bool, focus: Self, widget: &'a W + ) -> impl Widget + 'a { + Layers::new(move |add|{ + if parent_focus && focus == self { + add(&CORNERS)?; + add(&Background(Color::Rgb(60, 70, 50)))?; + } + add(widget) + }) + } +}