From 91d6bcc870a1e6eb23abe6434c373691d36cf84f Mon Sep 17 00:00:00 2001 From: unspeaker Date: Mon, 13 Jan 2025 18:05:55 +0100 Subject: [PATCH] wip: fold inwards --- tek/{edn => src}/arranger-keys.edn | 0 tek/{edn => src}/arranger-view.edn | 0 tek/src/arranger.rs | 376 --------------- tek/{edn => src}/groovebox-view.edn | 0 tek/src/lib.rs | 694 ++++++++++++++++++++++------ tek/src/mixer.rs | 25 - tek/{edn => src}/sequencer-view.edn | 0 7 files changed, 542 insertions(+), 553 deletions(-) rename tek/{edn => src}/arranger-keys.edn (100%) rename tek/{edn => src}/arranger-view.edn (100%) delete mode 100644 tek/src/arranger.rs rename tek/{edn => src}/groovebox-view.edn (100%) rename tek/{edn => src}/sequencer-view.edn (100%) diff --git a/tek/edn/arranger-keys.edn b/tek/src/arranger-keys.edn similarity index 100% rename from tek/edn/arranger-keys.edn rename to tek/src/arranger-keys.edn diff --git a/tek/edn/arranger-view.edn b/tek/src/arranger-view.edn similarity index 100% rename from tek/edn/arranger-view.edn rename to tek/src/arranger-view.edn diff --git a/tek/src/arranger.rs b/tek/src/arranger.rs deleted file mode 100644 index 15e404dd..00000000 --- a/tek/src/arranger.rs +++ /dev/null @@ -1,376 +0,0 @@ -use crate::*; -use EdnItem::*; -use ClockCommand::{Play, Pause}; -pub const TRACK_MIN_WIDTH: usize = 9; -impl HasJack for App { - fn jack (&self) -> &Arc> { &self.jack } -} -/// Hosts the JACK callback for a collection of tracks -pub struct TracksAudio<'a>( - // Track collection - pub &'a mut [ArrangerTrack], - /// Note buffer - pub &'a mut Vec, - /// Note chunk buffer - pub &'a mut Vec>>, -); -impl Audio for TracksAudio<'_> { - #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - let model = &mut self.0; - let note_buffer = &mut self.1; - let output_buffer = &mut self.2; - for track in model.iter_mut() { - if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit { - return Control::Quit - } - } - Control::Continue - } -} -command!(|self: ClipCommand, state: App|match self { _ => todo!("clip command") }); -command!(|self: SceneCommand, state: App|match self { _ => todo!("scene command") }); -command!(|self: TrackCommand, state: App|match self { _ => todo!("track command") }); -#[derive(Clone, Debug)] pub enum ClipCommand { - Get(usize, usize), - Put(usize, usize, Option>>), - Enqueue(usize, usize), - Edit(Option>>), - SetLoop(usize, usize, bool), - SetColor(usize, usize, ItemPalette), -} -edn_command!(ClipCommand: |state: App| { - ("get" [a: usize - ,b: usize] Self::Get(a.unwrap(), b.unwrap())) - - ("put" [a: usize - ,b: usize - ,c: Option>>] Self::Put(a, b, c)) - - ("enqueue" [a: usize - ,b: usize] Self::Enqueue(a, b)) - - ("edit" [a: Option>>] Self::Edit(a)) - - ("loop" [a: usize - ,b: usize - ,c: bool] Self::SetLoop(a, b, c)) - - ("color" [a: usize - ,b: usize] Self::SetColor(a, b, ItemPalette::random())) -}); -#[derive(Clone, Debug)] pub enum SceneCommand { - Add, - Del(usize), - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), - SetColor(usize, ItemPalette), - Enqueue(usize), -} -edn_command!(SceneCommand: |state: App| { - ("add" [] Self::Add) - ("del" [a: usize] Self::Del(0)) - ("zoom" [a: usize] Self::SetZoom(a)) - ("color" [a: usize] Self::SetColor(a, ItemPalette::random())) - ("enqueue" [a: usize] Self::Enqueue(a)) - ("swap" [a: usize, b: usize] Self::Swap(a, b)) -}); -#[derive(Clone, Debug)] pub enum TrackCommand { - Add, - Del(usize), - Stop(usize), - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), - SetColor(usize, ItemPalette), -} -edn_command!(TrackCommand: |state: App| { - ("add" [] Self::Add) - ("size" [a: usize] Self::SetSize(a)) - ("zoom" [a: usize] Self::SetZoom(a)) - ("color" [a: usize] Self::SetColor(a, ItemPalette::random())) - ("del" [a: usize] Self::Del(a)) - ("stop" [a: usize] Self::Stop(a)) - ("swap" [a: usize, b: usize] Self::Swap(a, b)) -}); -pub trait Arrangement: HasClock + HasJack { - fn tracks (&self) -> &Vec; - fn tracks_mut (&mut self) -> &mut Vec; - fn scenes (&self) -> &Vec; - fn scenes_mut (&mut self) -> &mut Vec; - fn selected (&self) -> &ArrangerSelection; - fn selected_mut (&mut self) -> &mut ArrangerSelection; - - fn track_next_name (&self) -> Arc { - format!("Trk{:02}", self.tracks().len() + 1).into() - } - fn track (&self) -> Option<&ArrangerTrack> { - self.selected().track().and_then(|s|self.tracks().get(s)) - } - fn track_mut (&mut self) -> Option<&mut ArrangerTrack> { - self.selected().track().and_then(|s|self.tracks_mut().get_mut(s)) - } - fn track_del (&mut self, index: usize) { - self.tracks_mut().remove(index); - for scene in self.scenes_mut().iter_mut() { - scene.clips.remove(index); - } - } - fn track_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerTrack> - { - let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into()); - let track = ArrangerTrack { - width: (name.len() + 2).max(9), - color: color.unwrap_or_else(ItemPalette::random), - player: MidiPlayer::from(self.clock()), - name, - }; - self.tracks_mut().push(track); - let len = self.tracks().len(); - let index = len - 1; - for scene in self.scenes_mut().iter_mut() { - while scene.clips.len() < len { - scene.clips.push(None); - } - } - Ok(&mut self.tracks_mut()[index]) - } - fn tracks_add ( - &mut self, - count: usize, - width: usize, - midi_from: &[PortConnection], - midi_to: &[PortConnection], - ) -> Usually<()> { - let jack = self.jack().clone(); - let track_color_1 = ItemColor::random(); - let track_color_2 = ItemColor::random(); - for i in 0..count { - let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); - let mut track = self.track_add(None, Some(color))?; - track.width = width; - let port = JackPort::::new(&jack, &format!("{}I", &track.name), midi_from)?; - track.player.midi_ins.push(port); - let port = JackPort::::new(&jack, &format!("{}O", &track.name), midi_to)?; - track.player.midi_outs.push(port); - } - Ok(()) - } - - fn scene_default_name (&self) -> Arc { - format!("Sc{:3>}", self.scenes().len() + 1).into() - } - fn scene (&self) -> Option<&ArrangerScene> { - self.selected().scene().and_then(|s|self.scenes().get(s)) - } - fn scene_mut (&mut self) -> Option<&mut ArrangerScene> { - self.selected().scene().and_then(|s|self.scenes_mut().get_mut(s)) - } - fn scene_del (&mut self, index: usize) { - todo!("delete scene"); - } - fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerScene> - { - let scene = ArrangerScene { - name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), - clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(ItemPalette::random), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok(&mut self.scenes_mut()[index]) - } - fn scenes_add (&mut self, n: usize) -> Usually<()> { - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..n { - let _scene = self.scene_add(None, Some( - scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() - ))?; - } - Ok(()) - } - fn activate (&mut self) -> Usually<()> { - let selected = self.selected().clone(); - match selected { - ArrangerSelection::Scene(s) => { - let mut clips = vec![]; - for (t, _) in self.tracks().iter().enumerate() { - clips.push(self.scenes()[s].clips[t].clone()); - } - for (t, track) in self.tracks_mut().iter_mut().enumerate() { - if track.player.play_clip.is_some() || clips[t].is_some() { - track.player.enqueue_next(clips[t].as_ref()); - } - } - if self.clock().is_stopped() { - self.clock().play_from(Some(0))?; - } - }, - ArrangerSelection::Clip(t, s) => { - let clip = self.scenes()[s].clips[t].clone(); - self.tracks_mut()[t].player.enqueue_next(clip.as_ref()); - }, - _ => {} - } - Ok(()) - } - fn clip (&self) -> Option>> { - self.scene()?.clips.get(self.selected().track()?)?.clone() - } - fn toggle_loop (&mut self) { - if let Some(clip) = self.clip() { - clip.write().unwrap().toggle_loop() - } - } - //fn randomize_color (&mut self) { - //match self.selected { - //ArrangerSelection::Mix => { self.color = ItemPalette::random() }, - //ArrangerSelection::Track(t) => { self.tracks[t].color = ItemPalette::random() }, - //ArrangerSelection::Scene(s) => { self.scenes[s].color = ItemPalette::random() }, - //ArrangerSelection::Clip(t, s) => if let Some(clip) = &self.scenes[s].clips[t] { - //clip.write().unwrap().color = ItemPalette::random(); - //} - //} - //} - -} - -#[derive(Default)] pub struct ArrangerScene { - /// Name of scene - pub(crate) name: Arc, - /// Clips in scene, one per track - pub(crate) clips: Vec>>>, - /// Identifying color of scene - pub(crate) color: ItemPalette, -} -impl ArrangerScene { - pub fn longest_name (scenes: &[Self]) -> usize { - scenes.iter().map(|s|s.name.len()).fold(0, usize::max) - } - /// Returns the pulse length of the longest clip in the scene - pub fn pulses (&self) -> usize { - self.clips.iter().fold(0, |a, p|{ - a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) - }) - } - /// Returns true if all clips in the scene are - /// currently playing on the given collection of tracks. - pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> bool { - self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() - .all(|(track_index, clip)|match clip { - Some(c) => tracks - .get(track_index) - .map(|track|{ - if let Some((_, Some(clip))) = track.player().play_clip() { - *clip.read().unwrap() == *c.read().unwrap() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) - } - pub fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } - } -} -#[derive(Debug)] pub struct ArrangerTrack { - /// Name of track - pub name: Arc, - /// Preferred width of track column - pub width: usize, - /// Identifying color of track - pub color: ItemPalette, - /// MIDI player state - pub player: MidiPlayer, -} -has_clock!(|self:ArrangerTrack|self.player.clock()); -has_player!(|self:ArrangerTrack|self.player); -impl ArrangerTrack { - fn longest_name (tracks: &[Self]) -> usize { - tracks.iter().map(|s|s.name.len()).fold(0, usize::max) - } - fn width_inc (&mut self) { - self.width += 1; - } - fn width_dec (&mut self) { - if self.width > TRACK_MIN_WIDTH { - self.width -= 1; - } - } -} -#[derive(PartialEq, Clone, Copy, Debug, Default)] -/// Represents the current user selection in the arranger -pub enum ArrangerSelection { - /// The whole mix is selected - #[default] 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 ArrangerSelection { - pub fn track (&self) -> Option { - use ArrangerSelection::*; - match self { - Clip(t, _) => Some(*t), - Track(t) => Some(*t), - _ => None - } - } - pub fn scene (&self) -> Option { - use ArrangerSelection::*; - match self { - Clip(_, s) => Some(*s), - Scene(s) => Some(*s), - _ => None - } - } - pub fn is_mix (&self) -> bool { matches!(self, Self::Mix) } - pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) } - pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) } - pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) } - pub fn description ( - &self, - tracks: &[ArrangerTrack], - scenes: &[ArrangerScene], - ) -> Arc { - format!("Selected: {}", match self { - Self::Mix => "Everything".to_string(), - Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)) - .unwrap_or_else(||"T??".into()), - Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)) - .unwrap_or_else(||"S??".into()), - Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { - (Some(_), Some(scene)) => match scene.clip(*t) { - Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), - None => format!("T{t} S{s}: Empty") - }, - _ => format!("T{t} S{s}: Empty"), - } - }).into() - } -} -impl Arrangement for App { - fn tracks (&self) -> &Vec { &self.tracks } - fn tracks_mut (&mut self) -> &mut Vec { &mut self.tracks } - fn scenes (&self) -> &Vec { &self.scenes } - fn scenes_mut (&mut self) -> &mut Vec { &mut self.scenes } - fn selected (&self) -> &ArrangerSelection { &self.selected } - fn selected_mut (&mut self) -> &mut ArrangerSelection { &mut self.selected } -} -//impl Arrangement for Arranger { - //fn tracks (&self) -> &Vec { &self.tracks } - //fn tracks_mut (&mut self) -> &mut Vec { &mut self.tracks } - //fn scenes (&self) -> &Vec { &self.scenes } - //fn scenes_mut (&mut self) -> &mut Vec { &mut self.scenes } - //fn selected (&self) -> &ArrangerSelection { &self.selected } - //fn selected_mut (&mut self) -> &mut ArrangerSelection { &mut self.selected } -//} diff --git a/tek/edn/groovebox-view.edn b/tek/src/groovebox-view.edn similarity index 100% rename from tek/edn/groovebox-view.edn rename to tek/src/groovebox-view.edn diff --git a/tek/src/lib.rs b/tek/src/lib.rs index 561027f0..2b652afa 100644 --- a/tek/src/lib.rs +++ b/tek/src/lib.rs @@ -9,8 +9,7 @@ pub type Usually = std::result::Result>; /// Standard optional result type. pub type Perhaps = std::result::Result, Box>; -pub mod arranger; pub use self::arranger::*; -pub mod mixer; pub use self::mixer::*; +pub mod mixer; pub use self::mixer::*; pub use ::tek_tui::{ *, tek_edn::*, @@ -67,14 +66,34 @@ pub(crate) use std::ffi::OsString; pub audio_ins: Vec>, pub audio_outs: Vec>, pub note_buf: Vec, - pub tracks: Vec, - pub scenes: Vec, + pub tracks: Vec, + pub scenes: Vec, pub selected: ArrangerSelection, pub splits: Vec, pub size: Measure, pub perf: PerfModel, pub compact: bool, } +#[derive(Debug, Default)] struct Meter<'a>(pub &'a str, pub f32); +#[derive(Debug, Default)] struct Meters<'a>(pub &'a[f32]); +#[derive(Debug, Default)] struct Track { + /// Name of track + name: Arc, + /// Preferred width of track column + width: usize, + /// Identifying color of track + color: ItemPalette, + /// MIDI player state + player: MidiPlayer, +} +#[derive(Default)] pub struct Scene { + /// Name of scene + pub(crate) name: Arc, + /// Clips in scene, one per track + pub(crate) clips: Vec>>>, + /// Identifying color of scene + pub(crate) color: ItemPalette, +} impl App { pub fn sequencer ( jack: &Arc>, @@ -85,7 +104,7 @@ impl App { midi_tos: &[PortConnection], ) -> Self { Self { - edn: include_str!("../edn/sequencer-view.edn").to_string(), + edn: include_str!("./sequencer-view.edn").to_string(), jack: jack.clone(), pool: Some(pool), editor: Some(editor), @@ -108,7 +127,7 @@ impl App { audio_tos: &[&[PortConnection]], ) -> Self { Self { - edn: include_str!("../edn/groovebox-view.edn").to_string(), + edn: include_str!("./groovebox-view.edn").to_string(), sampler: Some(sampler), ..Self::sequencer( jack, pool, editor, @@ -130,7 +149,7 @@ impl App { track_width: usize, ) -> Self { let mut arranger = Self { - edn: include_str!("../edn/arranger-view.edn").to_string(), + edn: include_str!("./arranger-view.edn").to_string(), ..Self::groovebox( jack, pool, editor, None, midi_froms, midi_tos, @@ -141,7 +160,205 @@ impl App { arranger.tracks_add(tracks, track_width, &[], &[]); arranger } + fn compact (&self) -> bool { false } + fn editor (&self) -> impl Content + '_ { &self.editor } + fn w (&self) -> u16 { self.tracks_with_sizes().last().map(|x|x.3 as u16).unwrap_or(0) } + fn pool (&self) -> impl Content + use<'_> { self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) } + fn sample <'a> (&'a self) -> impl Content + 'a { + let compact = self.is_editing(); + if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) { + let note_pt = editor.note_point(); + let sample_h = if compact { 0 } else { 5 }; + return Some(Max::y(sample_h, Fill::x(sampler.viewer(note_pt)))) + } + None + } + fn sampler (&self) -> impl Content + use<'_> { + let compact = self.is_editing(); + if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) { + let note_pt = editor.note_point(); + let w = if compact { 4 } else { 40 }; + let y = if compact { 1 } else { 0 }; + return Some(Fixed::x(w, Push::y(y, Fill::y(sampler.list(compact, editor))))) + } + None + } + fn row <'a> ( + &'a self, w: u16, h: u16, a: impl Content + 'a, b: impl Content + 'a + ) -> impl Content + 'a { + Fixed::y(h, Bsp::e( + Fixed::xy(self.sidebar_w() as u16, h, a), + Fill::x(Align::c(Fixed::xy(w, h, b))) + )) + } + pub fn tracks_with_sizes (&self) + -> impl Iterator + { + tracks_with_sizes(self.tracks.iter(), match self.selected { + ArrangerSelection::Track(t) if self.is_editing() => Some(t), + ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t), + _ => None + }, self.editor_w()) + } + pub fn scenes_with_sizes (&self, h: usize) + -> impl Iterator + { + scenes_with_sizes(self.scenes.iter(), &self.selected, self.is_editing(), 2, 15) + } + fn is_editing (&self) -> bool { + self.editing.load(Relaxed) + } + fn editor_w (&self) -> usize { + let editor = self.editor.as_ref().expect("missing editor"); + (5 + (editor.time_len().get() / editor.time_zoom().get())) + .min(self.size.w().saturating_sub(20)) + .max(16) + } + fn sidebar_w (&self) -> u16 { + let w = self.size.w(); + let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + let w = if self.is_editing() { 8 } else { w }; + w + } } +impl HasJack for App { fn jack (&self) -> &Arc> { &self.jack } } +has_size!(|self: App|&self.size); +has_clock!(|self: App|&self.clock); +has_clips!(|self: App|self.pool.as_ref().expect("no clip pool").clips); +has_clock!(|self:Track|self.player.clock()); +has_player!(|self:Track|self.player); +has_editor!(|self: App|self.editor.as_ref().expect("no editor")); +edn_provide!(u16: |self: App|{ + ":sample-h" => if self.compact() { 0 } else { 5 }, + ":samples-w" => if self.compact() { 4 } else { 11 }, + ":samples-y" => if self.compact() { 1 } else { 0 }, + ":pool-w" => if self.compact() { 5 } else { + let w = self.size.w(); + if w > 60 { 20 } else if w > 40 { 15 } else { 10 } + } +}); +edn_provide!(Color: |self: App| { _ => return None }); +edn_provide!(usize: |self: App| { _ => return None }); +edn_provide!(isize: |self: App| { _ => return None }); +edn_provide!(bool: |self: App| { _ => return None }); +edn_provide!(ArrangerSelection: |self: App| { _ => return None }); +edn_provide!(Arc>: |self: App| { _ => return None }); +edn_provide!(Option>>: |self: App| { _ => return None }); +edn_provide!('a: Box + 'a>: |self: App|{ + ":editor" => (&self.editor).boxed(), + ":pool" => self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))).boxed(), + ":sample" => self.sample().boxed(), + ":sampler" => self.sampler().boxed(), + ":status" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(), + ":toolbar" => Fill::x(Fixed::y(2, Align::x(ClockView::new(true, &self.clock)))).boxed(), + ":tracks" => self.row(self.w(), 3, + track_header(&self), track_cells(&self)).boxed(), + ":inputs" => self.row(self.w(), 3, + input_header(&self), input_cells(&self)).boxed(), + ":outputs" => self.row(self.w(), 3, + output_header(&self), output_cells(&self)).boxed(), + ":scenes" => self.row(self.w(), self.size.h().saturating_sub(9) as u16, + scene_header(&self), scene_cells(&self)).boxed(), +}); +render!(TuiOut: (self: App) => self.size.of(EdnView::from_source(self, self.edn.as_ref()))); +handle!(TuiIn: |self: App, input| Ok(None)); +#[derive(Clone, Debug)] pub enum AppCommand { + Clear, + Clip(ClipCommand), + Clock(ClockCommand), + Color(ItemPalette), + Compact(Option), + Editor(MidiEditCommand), + Enqueue(Option>>), + History(isize), + Pool(PoolCommand), + Sampler(SamplerCommand), + Scene(SceneCommand), + Select(ArrangerSelection), + StopAll, + Track(TrackCommand), + Zoom(Option), +} +edn_command!(AppCommand: |state: App| { + ("clear" [] Self::Clear) + ("stop-all" [] Self::StopAll) + ("compact" [c: bool ] Self::Compact(c)) + ("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default())) + ("history" [d: isize] Self::History(d.unwrap_or(0))) + ("zoom" [z: usize] Self::Zoom(z)) + ("select" [s: ArrangerSelection] Self::Select(s.expect("no selection"))) + ("enqueue" [c: Arc>] Self::Enqueue(c)) + ("clock" [a, ..b] Self::Clock(ClockCommand::from_edn(state, &a.to_ref(), b))) + ("track" [a, ..b] Self::Track(TrackCommand::from_edn(state, &a.to_ref(), b))) + ("scene" [a, ..b] Self::Scene(SceneCommand::from_edn(state, &a.to_ref(), b))) + ("clip" [a, ..b] Self::Clip(ClipCommand::from_edn(state, &a.to_ref(), b))) + ("pool" [a, ..b] Self::Pool(PoolCommand::from_edn(state.pool.as_ref().expect("no pool"), &a.to_ref(), b))) + ("editor" [a, ..b] Self::Editor(MidiEditCommand::from_edn(state.editor.as_ref().expect("no editor"), &a.to_ref(), b))) + ("sampler" [a, ..b] Self::Sampler(SamplerCommand::from_edn(state.sampler.as_ref().expect("no sampler"), &a.to_ref(), b))) +}); +#[derive(Clone, Debug)] pub enum ClipCommand { + Get(usize, usize), + Put(usize, usize, Option>>), + Enqueue(usize, usize), + Edit(Option>>), + SetLoop(usize, usize, bool), + SetColor(usize, usize, ItemPalette), +} +edn_command!(ClipCommand: |state: App| { + ("get" [a: usize + ,b: usize] Self::Get(a.unwrap(), b.unwrap())) + + ("put" [a: usize + ,b: usize + ,c: Option>>] Self::Put(a.unwrap(), b.unwrap(), c.unwrap())) + + ("enqueue" [a: usize + ,b: usize] Self::Enqueue(a.unwrap(), b.unwrap())) + + ("edit" [a: Option>>] Self::Edit(a.unwrap())) + + ("loop" [a: usize + ,b: usize + ,c: bool] Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())) + + ("color" [a: usize + ,b: usize] Self::SetColor(a.unwrap(), b.unwrap(), ItemPalette::random())) +}); +#[derive(Clone, Debug)] pub enum SceneCommand { + Add, + Del(usize), + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), + SetColor(usize, ItemPalette), + Enqueue(usize), +} +edn_command!(SceneCommand: |state: App| { + ("add" [] Self::Add) + ("del" [a: usize] Self::Del(0)) + ("zoom" [a: usize] Self::SetZoom(a.unwrap())) + ("color" [a: usize] Self::SetColor(a.unwrap(), ItemPalette::random())) + ("enqueue" [a: usize] Self::Enqueue(a.unwrap())) + ("swap" [a: usize, b: usize] Self::Swap(a.unwrap(), b.unwrap())) +}); +#[derive(Clone, Debug)] pub enum TrackCommand { + Add, + Del(usize), + Stop(usize), + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), + SetColor(usize, ItemPalette), +} +edn_command!(TrackCommand: |state: App| { + ("add" [] Self::Add) + ("size" [a: usize] Self::SetSize(a.unwrap())) + ("zoom" [a: usize] Self::SetZoom(a.unwrap())) + ("color" [a: usize] Self::SetColor(a.unwrap(), ItemPalette::random())) + ("del" [a: usize] Self::Del(a.unwrap())) + ("stop" [a: usize] Self::Stop(a.unwrap())) + ("swap" [a: usize, b: usize] Self::Swap(a.unwrap(), b.unwrap())) +}); audio!(|self: App, client, scope|{ // Start profiling cycle let t0 = self.perf.get_t0(); @@ -237,61 +454,32 @@ audio!(|self: App, client, scope|{ self.perf.update(t0, scope); Control::Continue }); -has_size!(|self: App|&self.size); -has_clock!(|self: App|&self.clock); -has_clips!(|self: App|self.pool.as_ref().expect("no clip pool").clips); -has_editor!(|self: App|self.editor.as_ref().expect("no editor")); -edn_provide!(u16: |self: App|{ - ":sample-h" => if self.compact() { 0 } else { 5 }, - ":samples-w" => if self.compact() { 4 } else { 11 }, - ":samples-y" => if self.compact() { 1 } else { 0 }, - ":pool-w" => if self.compact() { 5 } else { - let w = self.size.w(); - if w > 60 { 20 } else if w > 40 { 15 } else { 10 } +/// Hosts the JACK callback for a collection of tracks +struct TracksAudio<'a>( + // Track collection + &'a mut [Track], + /// Note buffer + &'a mut Vec, + /// Note chunk buffer + &'a mut Vec>>, +); +impl Audio for TracksAudio<'_> { + fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + let model = &mut self.0; + let note_buffer = &mut self.1; + let output_buffer = &mut self.2; + for track in model.iter_mut() { + if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit { + return Control::Quit + } + } + Control::Continue } -}); -edn_provide!(Color: |self: App| { _ => return None }); -edn_provide!(usize: |self: App| { _ => return None }); -edn_provide!(isize: |self: App| { _ => return None }); -edn_provide!(bool: |self: App| { _ => return None }); -edn_provide!(ArrangerSelection: |self: App| { _ => return None }); -edn_provide!(Arc>: |self: App| { _ => return None }); -handle!(TuiIn: |self: App, input| Ok(None)); -#[derive(Clone, Debug)] pub enum AppCommand { - Clear, - Clip(ClipCommand), - Clock(ClockCommand), - Color(ItemPalette), - Compact(Option), - Editor(MidiEditCommand), - Enqueue(Option>>), - History(isize), - Pool(PoolCommand), - Sampler(SamplerCommand), - Scene(SceneCommand), - Select(ArrangerSelection), - StopAll, - Track(TrackCommand), - Zoom(Option), } -edn_command!(AppCommand: |state: App| { - ("clear" [] Self::Clear) - ("stop-all" [] Self::StopAll) - ("compact" [c: bool ] Self::Compact(c)) - ("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default())) - ("history" [d: isize] Self::History(d.unwrap_or(0))) - ("zoom" [z: usize] Self::Zoom(z)) - ("select" [s: ArrangerSelection] Self::Select(s.expect("no selection"))) - ("enqueue" [c: Arc>] Self::Enqueue(c)) - ("clock" [a, ..b] Self::Clock(ClockCommand::from_edn(state, &a.to_ref(), b))) - ("track" [a, ..b] Self::Track(TrackCommand::from_edn(state, &a.to_ref(), b))) - ("scene" [a, ..b] Self::Scene(SceneCommand::from_edn(state, &a.to_ref(), b))) - ("clip" [a, ..b] Self::Clip(ClipCommand::from_edn(state, &a.to_ref(), b))) - ("pool" [a, ..b] Self::Pool(PoolCommand::from_edn(state.pool.as_ref().expect("no pool"), &a.to_ref(), b))) - ("editor" [a, ..b] Self::Editor(MidiEditCommand::from_edn(state.editor.as_ref().expect("no editor"), &a.to_ref(), b))) - ("sampler" [a, ..b] Self::Sampler(SamplerCommand::from_edn(state.sampler.as_ref().expect("no sampler"), &a.to_ref(), b))) -}); -command!(|self: AppCommand, state: App|match self { +command!(|self: ClipCommand, state: App|match self { _ => todo!("clip command") }); +command!(|self: SceneCommand, state: App|match self { _ => todo!("scene command") }); +command!(|self: TrackCommand, state: App|match self { _ => todo!("track command") }); +command!(|self: AppCommand, state: App|match self { Self::Clear => { todo!() }, Self::Zoom(_) => { todo!(); }, Self::History(delta) => { todo!("undo/redo") }, @@ -382,88 +570,13 @@ command!(|self: AppCommand, state: App|match self { } } }); -render!(TuiOut: (self: App) => self.size.of(EdnView::from_source(self, self.edn.as_ref()))); -edn_provide!('a: Box + 'a>: |self: App|{ - ":editor" => (&self.editor).boxed(), - ":pool" => self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))).boxed(), - ":sample" => self.sample().boxed(), - ":sampler" => self.sampler().boxed(), - ":status" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(), - ":toolbar" => Fill::x(Fixed::y(2, Align::x(ClockView::new(true, &self.clock)))).boxed(), - ":tracks" => self.row(self.w(), 3, track_header(&self), track_cells(&self)).boxed(), - ":inputs" => self.row(self.w(), 3, input_header(&self), input_cells(&self)).boxed(), - ":outputs" => self.row(self.w(), 3, output_header(&self), output_cells(&self)).boxed(), - ":scenes" => self.scene_row(self.w(), self.size.h().saturating_sub(9) as u16).boxed(), -}); -impl App { - fn compact (&self) -> bool { false } - fn editor (&self) -> impl Content + '_ { &self.editor } - fn w (&self) -> u16 { self.tracks_with_sizes().last().map(|x|x.3 as u16).unwrap_or(0) } - fn pool (&self) -> impl Content + use<'_> { self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) } - fn sample <'a> (&'a self) -> impl Content + 'a { - let compact = self.is_editing(); - if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) { - let note_pt = editor.note_point(); - let sample_h = if compact { 0 } else { 5 }; - return Some(Max::y(sample_h, Fill::x(sampler.viewer(note_pt)))) - } - None - } - fn sampler (&self) -> impl Content + use<'_> { - let compact = self.is_editing(); - if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) { - let note_pt = editor.note_point(); - let w = if compact { 4 } else { 40 }; - let y = if compact { 1 } else { 0 }; - return Some(Fixed::x(w, Push::y(y, Fill::y(sampler.list(compact, editor))))) - } - None - } - fn row <'a> ( - &'a self, w: u16, h: u16, a: impl Content + 'a, b: impl Content + 'a - ) -> impl Content + 'a { - Fixed::y(h, Bsp::e( - Fixed::xy(self.sidebar_w() as u16, h, a), - Fill::x(Align::c(Fixed::xy(w, h, b))) - )) - } - pub fn tracks_with_sizes (&self) - -> impl Iterator - { - tracks_with_sizes(self.tracks.iter(), match self.selected { - ArrangerSelection::Track(t) if self.is_editing() => Some(t), - ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t), - _ => None - }, self.editor_w()) - } - pub fn scenes_with_sizes (&self, h: usize) - -> impl Iterator - { - scenes_with_sizes(self.scenes.iter(), &self.selected, self.is_editing(), 2, 15) - } - fn is_editing (&self) -> bool { - self.editing.load(Relaxed) - } - fn editor_w (&self) -> usize { - let editor = self.editor.as_ref().expect("missing editor"); - (5 + (editor.time_len().get() / editor.time_zoom().get())) - .min(self.size.w().saturating_sub(20)) - .max(16) - } - fn sidebar_w (&self) -> u16 { - let w = self.size.w(); - let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - let w = if self.is_editing() { 8 } else { w }; - w - } -} pub fn scenes_with_sizes <'a>( - scenes: impl Iterator + 'a, + scenes: impl Iterator + 'a, selected: &'a ArrangerSelection, editing: bool, scene_height: usize, scene_larger: usize, -) -> impl Iterator + 'a { +) -> impl Iterator + 'a { let mut y = 0; let (selected_track, selected_scene) = match selected { ArrangerSelection::Clip(t, s) => (Some(t), Some(s)), @@ -478,10 +591,10 @@ pub fn scenes_with_sizes <'a>( }) } pub fn tracks_with_sizes <'a> ( - tracks: impl Iterator, + tracks: impl Iterator, active: Option, bigger: usize -) -> impl Iterator { +) -> impl Iterator { let mut x = 0; tracks.enumerate().map(move |(index, track)|{ let width = if Some(index) == active { bigger } else { track.width.max(8) }; @@ -491,17 +604,10 @@ pub fn tracks_with_sizes <'a> ( }) } pub fn track_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - (||Tui::bg(TuiTheme::g(32), Tui::bold(true, Bsp::s( - row!( - Tui::fg(TuiTheme::g(128), "add "), - Tui::fg(TuiTheme::orange(), "t"), - Tui::fg(TuiTheme::g(128), "rack"), - ), - row!( - Tui::fg(TuiTheme::orange(), "a"), - Tui::fg(TuiTheme::g(128), "dd scene"), - ), - ))).boxed()).into() + (||Tui::bg(TuiTheme::g(32), Bsp::s( + help_tag("add ", "t", "rack"), + help_tag("", "a", "dd scene"), + )).boxed()).into() } pub fn track_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { let iter = ||state.tracks_with_sizes(); @@ -518,7 +624,7 @@ pub fn track_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { )) })).boxed()).into() } -fn help_tag <'a>(before: &'a str, key: &'a str, after: &'a str) -> impl Content + 'a { +fn help_tag <'a> (before: &'a str, key: &'a str, after: &'a str) -> impl Content + 'a { let lo = TuiTheme::g(128); let hi = TuiTheme::orange(); Tui::bold(true, row!(Tui::fg(lo, before), Tui::fg(hi, key), Tui::fg(lo, after))) @@ -632,7 +738,7 @@ fn scene_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { }))).boxed()).into() } fn cell_clip <'a> ( - scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16 + scene: &'a Scene, index: usize, track: &'a Track, w: u16, h: u16 ) -> impl Content + use<'a> { scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{ let clip = clip.read().unwrap(); @@ -668,6 +774,290 @@ fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content { fn cell > (color: ItemPalette, field: T) -> impl Content { Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field)) } +pub const TRACK_MIN_WIDTH: usize = 9; +pub trait Arrangement: HasClock + HasJack { + fn tracks (&self) -> &Vec; + fn tracks_mut (&mut self) -> &mut Vec; + fn scenes (&self) -> &Vec; + fn scenes_mut (&mut self) -> &mut Vec; + fn selected (&self) -> &ArrangerSelection; + fn selected_mut (&mut self) -> &mut ArrangerSelection; + + fn track_next_name (&self) -> Arc { + format!("Trk{:02}", self.tracks().len() + 1).into() + } + fn track (&self) -> Option<&Track> { + self.selected().track().and_then(|s|self.tracks().get(s)) + } + fn track_mut (&mut self) -> Option<&mut Track> { + self.selected().track().and_then(|s|self.tracks_mut().get_mut(s)) + } + fn track_del (&mut self, index: usize) { + self.tracks_mut().remove(index); + for scene in self.scenes_mut().iter_mut() { + scene.clips.remove(index); + } + } + fn track_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut Track> + { + let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into()); + let track = Track { + width: (name.len() + 2).max(9), + color: color.unwrap_or_else(ItemPalette::random), + player: MidiPlayer::from(self.clock()), + name, + }; + self.tracks_mut().push(track); + let len = self.tracks().len(); + let index = len - 1; + for scene in self.scenes_mut().iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); + } + } + Ok(&mut self.tracks_mut()[index]) + } + fn tracks_add ( + &mut self, + count: usize, + width: usize, + midi_from: &[PortConnection], + midi_to: &[PortConnection], + ) -> Usually<()> { + let jack = self.jack().clone(); + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..count { + let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); + let mut track = self.track_add(None, Some(color))?; + track.width = width; + let port = JackPort::::new(&jack, &format!("{}I", &track.name), midi_from)?; + track.player.midi_ins.push(port); + let port = JackPort::::new(&jack, &format!("{}O", &track.name), midi_to)?; + track.player.midi_outs.push(port); + } + Ok(()) + } + + fn scene_default_name (&self) -> Arc { + format!("Sc{:3>}", self.scenes().len() + 1).into() + } + fn scene (&self) -> Option<&Scene> { + self.selected().scene().and_then(|s|self.scenes().get(s)) + } + fn scene_mut (&mut self) -> Option<&mut Scene> { + self.selected().scene().and_then(|s|self.scenes_mut().get_mut(s)) + } + fn scene_del (&mut self, index: usize) { + todo!("delete scene"); + } + fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut Scene> + { + let scene = Scene { + name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), + clips: vec![None;self.tracks().len()], + color: color.unwrap_or_else(ItemPalette::random), + }; + self.scenes_mut().push(scene); + let index = self.scenes().len() - 1; + Ok(&mut self.scenes_mut()[index]) + } + fn scenes_add (&mut self, n: usize) -> Usually<()> { + let scene_color_1 = ItemColor::random(); + let scene_color_2 = ItemColor::random(); + for i in 0..n { + let _scene = self.scene_add(None, Some( + scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() + ))?; + } + Ok(()) + } + fn activate (&mut self) -> Usually<()> { + let selected = self.selected().clone(); + match selected { + ArrangerSelection::Scene(s) => { + let mut clips = vec![]; + for (t, _) in self.tracks().iter().enumerate() { + clips.push(self.scenes()[s].clips[t].clone()); + } + for (t, track) in self.tracks_mut().iter_mut().enumerate() { + if track.player.play_clip.is_some() || clips[t].is_some() { + track.player.enqueue_next(clips[t].as_ref()); + } + } + if self.clock().is_stopped() { + self.clock().play_from(Some(0))?; + } + }, + ArrangerSelection::Clip(t, s) => { + let clip = self.scenes()[s].clips[t].clone(); + self.tracks_mut()[t].player.enqueue_next(clip.as_ref()); + }, + _ => {} + } + Ok(()) + } + fn clip (&self) -> Option>> { + self.scene()?.clips.get(self.selected().track()?)?.clone() + } + fn toggle_loop (&mut self) { + if let Some(clip) = self.clip() { + clip.write().unwrap().toggle_loop() + } + } + //fn randomize_color (&mut self) { + //match self.selected { + //ArrangerSelection::Mix => { self.color = ItemPalette::random() }, + //ArrangerSelection::Track(t) => { self.tracks[t].color = ItemPalette::random() }, + //ArrangerSelection::Scene(s) => { self.scenes[s].color = ItemPalette::random() }, + //ArrangerSelection::Clip(t, s) => if let Some(clip) = &self.scenes[s].clips[t] { + //clip.write().unwrap().color = ItemPalette::random(); + //} + //} + //} + +} + +impl Scene { + pub fn longest_name (scenes: &[Self]) -> usize { + scenes.iter().map(|s|s.name.len()).fold(0, usize::max) + } + /// Returns the pulse length of the longest clip in the scene + pub fn pulses (&self) -> usize { + self.clips.iter().fold(0, |a, p|{ + a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) + }) + } + /// Returns true if all clips in the scene are + /// currently playing on the given collection of tracks. + pub fn is_playing (&self, tracks: &[Track]) -> bool { + self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() + .all(|(track_index, clip)|match clip { + Some(c) => tracks + .get(track_index) + .map(|track|{ + if let Some((_, Some(clip))) = track.player().play_clip() { + *clip.read().unwrap() == *c.read().unwrap() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) + } + pub fn clip (&self, index: usize) -> Option<&Arc>> { + match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } + } +} +impl Track { + fn longest_name (tracks: &[Self]) -> usize { + tracks.iter().map(|s|s.name.len()).fold(0, usize::max) + } + fn width_inc (&mut self) { + self.width += 1; + } + fn width_dec (&mut self) { + if self.width > TRACK_MIN_WIDTH { + self.width -= 1; + } + } +} +#[derive(PartialEq, Clone, Copy, Debug, Default)] +/// Represents the current user selection in the arranger +pub enum ArrangerSelection { + /// The whole mix is selected + #[default] 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 ArrangerSelection { + pub fn track (&self) -> Option { + use ArrangerSelection::*; + match self { + Clip(t, _) => Some(*t), + Track(t) => Some(*t), + _ => None + } + } + pub fn scene (&self) -> Option { + use ArrangerSelection::*; + match self { + Clip(_, s) => Some(*s), + Scene(s) => Some(*s), + _ => None + } + } + pub fn is_mix (&self) -> bool { matches!(self, Self::Mix) } + pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) } + pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) } + pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) } + pub fn description ( + &self, + tracks: &[Track], + scenes: &[Scene], + ) -> Arc { + format!("Selected: {}", match self { + Self::Mix => "Everything".to_string(), + Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)) + .unwrap_or_else(||"T??".into()), + Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)) + .unwrap_or_else(||"S??".into()), + Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { + (Some(_), Some(scene)) => match scene.clip(*t) { + Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), + None => format!("T{t} S{s}: Empty") + }, + _ => format!("T{t} S{s}: Empty"), + } + }).into() + } +} +impl Arrangement for App { + fn tracks (&self) -> &Vec { &self.tracks } + fn tracks_mut (&mut self) -> &mut Vec { &mut self.tracks } + fn scenes (&self) -> &Vec { &self.scenes } + fn scenes_mut (&mut self) -> &mut Vec { &mut self.scenes } + fn selected (&self) -> &ArrangerSelection { &self.selected } + fn selected_mut (&mut self) -> &mut ArrangerSelection { &mut self.selected } +} +//impl Arrangement for Arranger { + //fn tracks (&self) -> &Vec { &self.tracks } + //fn tracks_mut (&mut self) -> &mut Vec { &mut self.tracks } + //fn scenes (&self) -> &Vec { &self.scenes } + //fn scenes_mut (&mut self) -> &mut Vec { &mut self.scenes } + //fn selected (&self) -> &ArrangerSelection { &self.selected } + //fn selected_mut (&mut self) -> &mut ArrangerSelection { &mut self.selected } +//} +render!(TuiOut: (self: Meter<'a>) => col!( + Field(TuiTheme::g(128).into(), self.0, format!("{:>+9.3}", self.1)), + Fixed::xy(if self.1 >= 0.0 { 13 } + else if self.1 >= -1.0 { 12 } + else if self.1 >= -2.0 { 11 } + else if self.1 >= -3.0 { 10 } + else if self.1 >= -4.0 { 9 } + else if self.1 >= -6.0 { 8 } + else if self.1 >= -9.0 { 7 } + else if self.1 >= -12.0 { 6 } + else if self.1 >= -15.0 { 5 } + else if self.1 >= -20.0 { 4 } + else if self.1 >= -25.0 { 3 } + else if self.1 >= -30.0 { 2 } + else if self.1 >= -40.0 { 1 } + else { 0 }, 1, Tui::bg(if self.1 >= 0.0 { Color::Red } + else if self.1 >= -3.0 { Color::Yellow } + else { Color::Green }, ())))); +render!(TuiOut: (self: Meters<'a>) => col!( + format!("L/{:>+9.3}", self.0[0]), + format!("R/{:>+9.3}", self.0[1]))); + #[cfg(test)] fn test_tek () { // TODO } diff --git a/tek/src/mixer.rs b/tek/src/mixer.rs index e86b5b80..bae4b4a2 100644 --- a/tek/src/mixer.rs +++ b/tek/src/mixer.rs @@ -1,30 +1,5 @@ use crate::*; -pub struct Meter<'a>(pub &'a str, pub f32); -render!(TuiOut: (self: Meter<'a>) => col!( - Field(TuiTheme::g(128).into(), self.0, format!("{:>+9.3}", self.1)), - Fixed::xy(if self.1 >= 0.0 { 13 } - else if self.1 >= -1.0 { 12 } - else if self.1 >= -2.0 { 11 } - else if self.1 >= -3.0 { 10 } - else if self.1 >= -4.0 { 9 } - else if self.1 >= -6.0 { 8 } - else if self.1 >= -9.0 { 7 } - else if self.1 >= -12.0 { 6 } - else if self.1 >= -15.0 { 5 } - else if self.1 >= -20.0 { 4 } - else if self.1 >= -25.0 { 3 } - else if self.1 >= -30.0 { 2 } - else if self.1 >= -40.0 { 1 } - else { 0 }, 1, Tui::bg(if self.1 >= 0.0 { Color::Red } - else if self.1 >= -3.0 { Color::Yellow } - else { Color::Green }, ())))); - -pub struct Meters<'a>(pub &'a[f32]); -render!(TuiOut: (self: Meters<'a>) => col!( - format!("L/{:>+9.3}", self.0[0]), - format!("R/{:>+9.3}", self.0[1]))); - #[derive(Debug)] pub struct Mixer { /// JACK client handle (needs to not be dropped for standalone mode to work). diff --git a/tek/edn/sequencer-view.edn b/tek/src/sequencer-view.edn similarity index 100% rename from tek/edn/sequencer-view.edn rename to tek/src/sequencer-view.edn