From ceaaeb1fc728b1ffc58a84b061a3e6ca20bf622c Mon Sep 17 00:00:00 2001 From: unspeaker Date: Mon, 13 Jan 2025 18:30:46 +0100 Subject: [PATCH] wip: flatten more --- tek/src/lib.rs | 1056 +++++++++++++++++++++++------------------------- 1 file changed, 512 insertions(+), 544 deletions(-) diff --git a/tek/src/lib.rs b/tek/src/lib.rs index 2b652afa..e5c2c0e9 100644 --- a/tek/src/lib.rs +++ b/tek/src/lib.rs @@ -68,14 +68,76 @@ pub(crate) use std::ffi::OsString; pub note_buf: Vec, pub tracks: Vec, pub scenes: Vec, - pub selected: ArrangerSelection, + pub selected: Selection, pub splits: Vec, pub size: Measure, pub perf: PerfModel, pub compact: bool, } +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!(Selection: |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, + self.track_header(), self.track_cells()).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, + self.scene_header(), self.scene_cells()).boxed(), +}); +render!(TuiOut: (self: App) => self.size.of(EdnView::from_source(self, self.edn.as_ref()))); +handle!(TuiIn: |self: App, input| Ok(None)); #[derive(Debug, Default)] 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 }, ())))); #[derive(Debug, Default)] 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, Default)] struct Track { /// Name of track name: Arc, @@ -86,22 +148,64 @@ pub(crate) use std::ffi::OsString; /// MIDI player state player: MidiPlayer, } -#[derive(Default)] pub struct Scene { +impl Track { + const MIN_WIDTH: usize = 9; + 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(Debug, Default)] struct Scene { /// Name of scene - pub(crate) name: Arc, + name: Arc, /// Clips in scene, one per track - pub(crate) clips: Vec>>>, + clips: Vec>>>, /// Identifying color of scene - pub(crate) color: ItemPalette, + color: ItemPalette, +} +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 App { pub fn sequencer ( - jack: &Arc>, - pool: MidiPool, - editor: MidiEditor, - player: Option, - midi_froms: &[PortConnection], - midi_tos: &[PortConnection], + jack: &Arc>, pool: MidiPool, editor: MidiEditor, + player: Option, midi_froms: &[PortConnection], midi_tos: &[PortConnection], ) -> Self { Self { edn: include_str!("./sequencer-view.edn").to_string(), @@ -116,15 +220,9 @@ impl App { } } pub fn groovebox ( - jack: &Arc>, - pool: MidiPool, - editor: MidiEditor, - player: Option, - midi_froms: &[PortConnection], - midi_tos: &[PortConnection], - sampler: Sampler, - audio_froms: &[&[PortConnection]], - audio_tos: &[&[PortConnection]], + jack: &Arc>, pool: MidiPool, editor: MidiEditor, + player: Option, midi_froms: &[PortConnection], midi_tos: &[PortConnection], + sampler: Sampler, audio_froms: &[&[PortConnection]], audio_tos: &[&[PortConnection]], ) -> Self { Self { edn: include_str!("./groovebox-view.edn").to_string(), @@ -136,17 +234,10 @@ impl App { } } pub fn arranger ( - jack: &Arc>, - pool: MidiPool, - editor: MidiEditor, - midi_froms: &[PortConnection], - midi_tos: &[PortConnection], - sampler: Sampler, - audio_froms: &[&[PortConnection]], - audio_tos: &[&[PortConnection]], - scenes: usize, - tracks: usize, - track_width: usize, + jack: &Arc>, pool: MidiPool, editor: MidiEditor, + midi_froms: &[PortConnection], midi_tos: &[PortConnection], + sampler: Sampler, audio_froms: &[&[PortConnection]], audio_tos: &[&[PortConnection]], + scenes: usize, tracks: usize, track_width: usize, ) -> Self { let mut arranger = Self { edn: include_str!("./arranger-view.edn").to_string(), @@ -191,19 +282,11 @@ impl App { 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()) + fn tracks_with_sizes (&self) -> impl Iterator { + self.tracks_sizes(self.is_editing(), 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 scenes_with_sizes (&self, h: usize) -> impl Iterator { + self.scenes_sizes(self.is_editing(), 2, 15) } fn is_editing (&self) -> bool { self.editing.load(Relaxed) @@ -221,47 +304,6 @@ impl App { 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), @@ -274,7 +316,7 @@ handle!(TuiIn: |self: App, input| Ok(None)); Pool(PoolCommand), Sampler(SamplerCommand), Scene(SceneCommand), - Select(ArrangerSelection), + Select(Selection), StopAll, Track(TrackCommand), Zoom(Option), @@ -286,7 +328,7 @@ edn_command!(AppCommand: |state: App| { ("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"))) + ("select" [s: Selection] 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))) @@ -296,190 +338,7 @@ edn_command!(AppCommand: |state: App| { ("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(); - // Update transport clock - if Control::Quit == ClockAudio(self).process(client, scope) { - return Control::Quit - } - // Collect MIDI input (TODO preallocate) - let midi_in = self.midi_ins.iter() - .map(|port|port.port.iter(scope) - .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) - .collect::>()) - .collect::>(); - // Update standalone MIDI sequencer - if let Some(player) = self.player.as_mut() { - if Control::Quit == PlayerAudio( - player, - &mut self.note_buf, - &mut self.midi_buf, - ).process(client, scope) { - return Control::Quit - } - } - // Update standalone sampler - if let Some(sampler) = self.sampler.as_mut() { - if Control::Quit == SamplerAudio(sampler).process(client, scope) { - return Control::Quit - } - //for port in midi_in.iter() { - //for message in port.iter() { - //match message { - //Ok(M - //} - //} - //} - } - // TODO move these to editor and sampler?: - for port in midi_in.iter() { - for event in port.iter() { - match event { - (time, Ok(LiveEvent::Midi {message, ..})) => match message { - MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => { - editor.set_note_point(key.as_int() as usize); - }, - MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = ( - self.editor.as_ref(), - self.sampler.as_ref(), - ) => { - // TODO: give sampler its own cursor - if let Some(sample) = &sampler.mapped[editor.note_point()] { - sample.write().unwrap().handle_cc(*controller, *value) - } - } - _ =>{} - }, - _ =>{} - } - } - } - - // Update track sequencers - let tracks = &mut self.tracks; - let note_buf = &mut self.note_buf; - let midi_buf = &mut self.midi_buf; - if Control::Quit == TracksAudio(tracks, note_buf, midi_buf).process(client, scope) { - return Control::Quit - } - - // TODO: update timeline position in editor. - // must be in sync with clip's playback. since - // a clip can be on multiple tracks and launched - // at different times, add a playhead with the - // playing track's color. - //self.now.set(0.); - //if let ArrangerSelection::Clip(t, s) = self.selected { - //let clip = self.scenes.get(s).map(|scene|scene.clips.get(t)); - //if let Some(Some(Some(clip))) = clip { - //if let Some(track) = self.tracks().get(t) { - //if let Some((ref started_at, Some(ref playing))) = track.player.play_clip { - //let clip = clip.read().unwrap(); - //if *playing.read().unwrap() == *clip { - //let pulse = self.current().pulse.get(); - //let start = started_at.pulse.get(); - //let now = (pulse - start) % clip.length as f64; - //self.now.set(now); - //} - //} - //} - //} - //} - - // End profiling cycle - self.perf.update(t0, scope); - Control::Continue -}); -/// 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 - } -} -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 { +command!(|self: AppCommand, state: App|match self { Self::Clear => { todo!() }, Self::Zoom(_) => { todo!(); }, Self::History(delta) => { todo!("undo/redo") }, @@ -570,59 +429,188 @@ command!(|self: AppCommand, state: App|match self { } } }); -pub fn scenes_with_sizes <'a>( - scenes: impl Iterator + 'a, - selected: &'a ArrangerSelection, - editing: bool, - scene_height: usize, - scene_larger: usize, -) -> impl Iterator + 'a { - let mut y = 0; - let (selected_track, selected_scene) = match selected { - ArrangerSelection::Clip(t, s) => (Some(t), Some(s)), - _ => (None, None) - }; - scenes.enumerate().map(move|(s, scene)|{ - let active = editing && selected_track.is_some() && selected_scene == Some(&s); - let height = if active { scene_larger } else { scene_height }; - let data = (s, scene, y, y + height); - y += height; - data - }) +#[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), } -pub fn tracks_with_sizes <'a> ( - tracks: impl Iterator, - active: Option, - bigger: usize -) -> impl Iterator { - let mut x = 0; - tracks.enumerate().map(move |(index, track)|{ - let width = if Some(index) == active { bigger } else { track.width.max(8) }; - let data = (index, track, x, x + width); - x += width; - data - }) +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())) +}); +command!(|self: ClipCommand, state: App|match self { _ => todo!("clip command") }); +#[derive(Clone, Debug)] pub enum SceneCommand { + Add, + Del(usize), + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), + SetColor(usize, ItemPalette), + Enqueue(usize), } -pub fn track_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - (||Tui::bg(TuiTheme::g(32), Bsp::s( - help_tag("add ", "t", "rack"), - help_tag("", "a", "dd scene"), - )).boxed()).into() +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())) +}); +command!(|self: SceneCommand, state: App|match self { _ => todo!("scene command") }); +#[derive(Clone, Debug)] pub enum TrackCommand { + Add, + Del(usize), + Stop(usize), + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), + SetColor(usize, ItemPalette), } -pub fn track_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - let iter = ||state.tracks_with_sizes(); - (move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| { - let name = Push::x(1, &track.name); - let color = track.color; - let fg = color.lightest.rgb; - let bg = color.base.rgb; - let active = state.selected.track() == Some(i); - let bfg = if active { Color::Rgb(255,255,255) } else { Color::Rgb(0,0,0) }; - let border = Style::default().fg(bfg).bg(bg); - Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16, - Outer(border).enclose(Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::x(name))))) - )) - })).boxed()).into() +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())) +}); +command!(|self: TrackCommand, state: App|match self { _ => todo!("track command") }); +audio!(|self: App, client, scope|{ + // Start profiling cycle + let t0 = self.perf.get_t0(); + // Update transport clock + if Control::Quit == ClockAudio(self).process(client, scope) { + return Control::Quit + } + // Collect MIDI input (TODO preallocate) + let midi_in = self.midi_ins.iter() + .map(|port|port.port.iter(scope) + .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) + .collect::>()) + .collect::>(); + // Update standalone MIDI sequencer + if let Some(player) = self.player.as_mut() { + if Control::Quit == PlayerAudio( + player, + &mut self.note_buf, + &mut self.midi_buf, + ).process(client, scope) { + return Control::Quit + } + } + // Update standalone sampler + if let Some(sampler) = self.sampler.as_mut() { + if Control::Quit == SamplerAudio(sampler).process(client, scope) { + return Control::Quit + } + //for port in midi_in.iter() { + //for message in port.iter() { + //match message { + //Ok(M + //} + //} + //} + } + // TODO move these to editor and sampler?: + for port in midi_in.iter() { + for event in port.iter() { + match event { + (time, Ok(LiveEvent::Midi {message, ..})) => match message { + MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => { + editor.set_note_point(key.as_int() as usize); + }, + MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = ( + self.editor.as_ref(), + self.sampler.as_ref(), + ) => { + // TODO: give sampler its own cursor + if let Some(sample) = &sampler.mapped[editor.note_point()] { + sample.write().unwrap().handle_cc(*controller, *value) + } + } + _ =>{} + }, + _ =>{} + } + } + } + + // Update track sequencers + let tracks = &mut self.tracks; + let note_buf = &mut self.note_buf; + let midi_buf = &mut self.midi_buf; + if Control::Quit == TracksAudio(tracks, note_buf, midi_buf).process(client, scope) { + return Control::Quit + } + + // TODO: update timeline position in editor. + // must be in sync with clip's playback. since + // a clip can be on multiple tracks and launched + // at different times, add a playhead with the + // playing track's color. + //self.now.set(0.); + //if let Selection::Clip(t, s) = self.selected { + //let clip = self.scenes.get(s).map(|scene|scene.clips.get(t)); + //if let Some(Some(Some(clip))) = clip { + //if let Some(track) = self.tracks().get(t) { + //if let Some((ref started_at, Some(ref playing))) = track.player.play_clip { + //let clip = clip.read().unwrap(); + //if *playing.read().unwrap() == *clip { + //let pulse = self.current().pulse.get(); + //let start = started_at.pulse.get(); + //let now = (pulse - start) % clip.length as f64; + //self.now.set(now); + //} + //} + //} + //} + //} + + // End profiling cycle + self.perf.update(t0, scope); + Control::Continue +}); +/// 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 + } } fn help_tag <'a> (before: &'a str, key: &'a str, after: &'a str) -> impl Content + 'a { let lo = TuiTheme::g(128); @@ -667,76 +655,6 @@ fn output_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { )))) })).boxed()).into() } -fn scene_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - (||{ - let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); - let selected = state.selected.scene(); - Fill::y(Align::c(Map::new(||state.scenes_with_sizes(2), move|(_, scene, y1, y2), i| { - let h = (y2 - y1) as u16; - let name = format!("🭬{}", &scene.name); - let color = scene.color; - let active = selected == Some(i); - let mid = if active { color.light } else { color.base }; - let top = Some(last_color.read().unwrap().base.rgb); - let cell = phat_sel_3( - active, - Tui::bold(true, name.clone()), - Tui::bold(true, name), - top, - mid.rgb, - Color::Rgb(0, 0, 0) - ); - *last_color.write().unwrap() = color; - map_south(y1 as u16, h + 1, Fixed::y(h + 1, cell)) - }))).boxed() - }).into() -} -fn scene_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - let editing = state.is_editing(); - let tracks = move||state.tracks_with_sizes(); - let scenes = ||state.scenes_with_sizes(2); - let selected_track = state.selected.track(); - let selected_scene = state.selected.scene(); - (move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| { - let w = (x2 - x1) as u16; - let color: ItemPalette = track.color.dark.into(); - let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); - let cells = Map::new(scenes, move|(_, scene, y1, y2), s| { - let h = (y2 - y1) as u16; - let color = scene.color; - let (name, fg, bg) = if let Some(c) = &scene.clips[t] { - let c = c.read().unwrap(); - (c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb) - } else { - ("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32)) - }; - let last = last_color.read().unwrap().clone(); - let active = editing && selected_scene == Some(s) && selected_track == Some(t); - let editor = Thunk::new(||&state.editor); - let cell = Thunk::new(move||phat_sel_3( - selected_track == Some(t) && selected_scene == Some(s), - Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), - Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), - if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) { - None - } else { - Some(bg.into()) - }, - bg.into(), - bg.into(), - )); - let cell = Either(active, editor, cell); - *last_color.write().unwrap() = bg.into(); - map_south( - y1 as u16, - h + 1, - Fill::x(Fixed::y(h + 1, cell)) - ) - }); - let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed()); - Fixed::x(w, map_east(x1 as u16, w, column)) - }))).boxed()).into() -} fn cell_clip <'a> ( scene: &'a Scene, index: usize, track: &'a Track, w: u16, h: u16 ) -> impl Content + use<'a> { @@ -774,15 +692,85 @@ 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 { +impl HasSelection for App { + fn selected (&self) -> &Selection { &self.selected } + fn selected_mut (&mut self) -> &mut Selection { &mut self.selected } +} +pub trait HasSelection { + fn selected (&self) -> &Selection; + fn selected_mut (&mut self) -> &mut Selection; +} +/// Represents the current user selection in the arranger +#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { + /// 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 Selection { + 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 track (&self) -> Option { + use Selection::*; + match self { Clip(t, _) => Some(*t), Track(t) => Some(*t), _ => None } + } + pub fn scene (&self) -> Option { + use Selection::*; + match self { Clip(_, s) => Some(*s), Scene(s) => Some(*s), _ => None } + } + 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 HasTracks for App { + fn tracks (&self) -> &Vec { &self.tracks } + fn tracks_mut (&mut self) -> &mut Vec { &mut self.tracks } +} +pub trait HasTracks: HasSelection + HasScenes + 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 tracks_sizes ( + &self, + editing: bool, + bigger: usize + ) -> impl Iterator { + let mut x = 0; + let active = match self.selected() { + Selection::Track(t) if editing => Some(t), + Selection::Clip(t, _) if editing => Some(t), + _ => None + }; + self.tracks().iter().enumerate().map(move |(index, track)|{ + let width = if Some(&index) == active { bigger } else { track.width.max(8) }; + let data = (index, track, x, x + width); + x += width; + data + }) + } fn track_next_name (&self) -> Arc { format!("Trk{:02}", self.tracks().len() + 1).into() } @@ -839,7 +827,54 @@ pub trait Arrangement: HasClock + HasJack { } Ok(()) } - + fn track_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (||Tui::bg(TuiTheme::g(32), Bsp::s( + help_tag("add ", "t", "rack"), + help_tag("", "a", "dd scene"), + )).boxed()).into() + } + fn track_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + let iter = ||self.tracks_with_sizes(); + (move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| { + let name = Push::x(1, &track.name); + let color = track.color; + let fg = color.lightest.rgb; + let bg = color.base.rgb; + let active = self.selected.track() == Some(i); + let bfg = if active { Color::Rgb(255,255,255) } else { Color::Rgb(0,0,0) }; + let border = Style::default().fg(bfg).bg(bg); + Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16, + Outer(border).enclose(Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::x(name))))) + )) + })).boxed()).into() + } +} +impl HasScenes for App { + fn scenes (&self) -> &Vec { &self.scenes } + fn scenes_mut (&mut self) -> &mut Vec { &mut self.scenes } +} +pub trait HasScenes: HasSelection { + fn scenes (&self) -> &Vec; + fn scenes_mut (&mut self) -> &mut Vec; + fn scenes_sizes( + &self, + editing: bool, + scene_height: usize, + scene_larger: usize, + ) -> impl Iterator { + let mut y = 0; + let (selected_track, selected_scene) = match self.selected() { + Selection::Clip(t, s) => (Some(t), Some(s)), + _ => (None, None) + }; + self.scenes().iter().enumerate().map(move|(s, scene)|{ + let active = editing && selected_track.is_some() && selected_scene == Some(&s); + let height = if active { scene_larger } else { scene_height }; + let data = (s, scene, y, y + height); + y += height; + data + }) + } fn scene_default_name (&self) -> Arc { format!("Sc{:3>}", self.scenes().len() + 1).into() } @@ -874,10 +909,83 @@ pub trait Arrangement: HasClock + HasJack { } Ok(()) } + fn scene_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (||{ + let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); + let selected = self.selected.scene(); + Fill::y(Align::c(Map::new(||self.scenes_with_sizes(2), move|(_, scene, y1, y2), i| { + let h = (y2 - y1) as u16; + let name = format!("🭬{}", &scene.name); + let color = scene.color; + let active = selected == Some(i); + let mid = if active { color.light } else { color.base }; + let top = Some(last_color.read().unwrap().base.rgb); + let cell = phat_sel_3( + active, + Tui::bold(true, name.clone()), + Tui::bold(true, name), + top, + mid.rgb, + Color::Rgb(0, 0, 0) + ); + *last_color.write().unwrap() = color; + map_south(y1 as u16, h + 1, Fixed::y(h + 1, cell)) + }))).boxed() + }).into() + } + fn scene_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + let editing = self.is_editing(); + let tracks = move||self.tracks_with_sizes(); + let scenes = ||self.scenes_with_sizes(2); + let selected_track = self.selected.track(); + let selected_scene = self.selected.scene(); + (move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| { + let w = (x2 - x1) as u16; + let color: ItemPalette = track.color.dark.into(); + let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); + let cells = Map::new(scenes, move|(_, scene, y1, y2), s| { + let h = (y2 - y1) as u16; + let color = scene.color; + let (name, fg, bg) = if let Some(c) = &scene.clips[t] { + let c = c.read().unwrap(); + (c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb) + } else { + ("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32)) + }; + let last = last_color.read().unwrap().clone(); + let active = editing && selected_scene == Some(s) && selected_track == Some(t); + let editor = Thunk::new(||&self.editor); + let cell = Thunk::new(move||phat_sel_3( + selected_track == Some(t) && selected_scene == Some(s), + Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), + Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), + if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) { + None + } else { + Some(bg.into()) + }, + bg.into(), + bg.into(), + )); + let cell = Either(active, editor, cell); + *last_color.write().unwrap() = bg.into(); + map_south( + y1 as u16, + h + 1, + Fill::x(Fixed::y(h + 1, cell)) + ) + }); + let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed()); + Fixed::x(w, map_east(x1 as u16, w, column)) + }))).boxed()).into() + } +} +impl Arrangement for App {} +pub trait Arrangement: HasTracks + HasScenes + HasSelection + HasClock + HasJack { fn activate (&mut self) -> Usually<()> { let selected = self.selected().clone(); match selected { - ArrangerSelection::Scene(s) => { + Selection::Scene(s) => { let mut clips = vec![]; for (t, _) in self.tracks().iter().enumerate() { clips.push(self.scenes()[s].clips[t].clone()); @@ -891,7 +999,7 @@ pub trait Arrangement: HasClock + HasJack { self.clock().play_from(Some(0))?; } }, - ArrangerSelection::Clip(t, s) => { + Selection::Clip(t, s) => { let clip = self.scenes()[s].clips[t].clone(); self.tracks_mut()[t].player.enqueue_next(clip.as_ref()); }, @@ -909,155 +1017,15 @@ pub trait Arrangement: HasClock + HasJack { } //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] { + //Selection::Mix => { self.color = ItemPalette::random() }, + //Selection::Track(t) => { self.tracks[t].color = ItemPalette::random() }, + //Selection::Scene(s) => { self.scenes[s].color = ItemPalette::random() }, + //Selection::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 }