From 775fea2c087f9a0c7b5541ddb8d07a63906a2142 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 17 Dec 2024 00:55:21 +0100 Subject: [PATCH 001/905] phrase 0; stop all; loop_on->looped; remove trailers --- crates/tek/src/api/phrase.rs | 19 ++++++++++++++----- crates/tek/src/layout/align.rs | 4 ++-- crates/tek/src/layout/min_max.rs | 2 +- crates/tek/src/tui.rs | 2 +- crates/tek/src/tui/app_arranger.rs | 2 +- crates/tek/src/tui/app_sampler.rs | 2 +- crates/tek/src/tui/phrase_editor.rs | 2 +- crates/tek/src/tui/phrase_list.rs | 4 ++-- 8 files changed, 23 insertions(+), 14 deletions(-) diff --git a/crates/tek/src/api/phrase.rs b/crates/tek/src/api/phrase.rs index 1fed237d..d0d18322 100644 --- a/crates/tek/src/api/phrase.rs +++ b/crates/tek/src/api/phrase.rs @@ -117,7 +117,7 @@ pub struct Phrase { /// Notes in phrase pub notes: PhraseData, /// Whether to loop the phrase or play it once - pub loop_on: bool, + pub looped: bool, /// Start of loop pub loop_start: usize, /// Length of loop @@ -134,7 +134,7 @@ pub type PhraseData = Vec>; impl Phrase { pub fn new ( name: impl AsRef, - loop_on: bool, + looped: bool, length: usize, notes: Option, color: Option, @@ -145,7 +145,7 @@ impl Phrase { ppq: PPQ, length, notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), - loop_on, + looped, loop_start: 0, loop_length: length, percussive: true, @@ -161,7 +161,7 @@ impl Phrase { clone.uuid = uuid::Uuid::new_v4(); clone } - pub fn toggle_loop (&mut self) { self.loop_on = !self.loop_on; } + pub fn toggle_loop (&mut self) { self.looped = !self.looped; } pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { if pulse >= self.length { panic!("extend phrase first") } self.notes[pulse].push(message); @@ -180,7 +180,16 @@ impl Phrase { impl Default for Phrase { fn default () -> Self { - Self::new("null", false, 0, None, Some(ItemColor::from(Color::Rgb(0, 0, 0)).into())) + Self::new( + "Stop", + false, + 1, + Some(vec![vec![MidiMessage::Controller { + controller: 123.into(), + value: 0.into() + }]]), + Some(ItemColor::from(Color::Rgb(32, 32, 32)).into()) + ) } } diff --git a/crates/tek/src/layout/align.rs b/crates/tek/src/layout/align.rs index 48c64874..2e11791a 100644 --- a/crates/tek/src/layout/align.rs +++ b/crates/tek/src/layout/align.rs @@ -27,11 +27,11 @@ pub enum Align { /// Draw at upper left corner of contaier NW(L), /// Draw at center of upper edge of container - N(L), + N(L), /// Draw at right left corner of contaier NE(L), /// Draw at center of left edge of container - W(L), + W(L), /// Draw at center of right edge of container E(L), /// Draw at lower left corner of container diff --git a/crates/tek/src/layout/min_max.rs b/crates/tek/src/layout/min_max.rs index 42312bab..f0b5f26f 100644 --- a/crates/tek/src/layout/min_max.rs +++ b/crates/tek/src/layout/min_max.rs @@ -7,7 +7,7 @@ pub trait LayoutMinMax { Min::X(x, w) } fn min_y > (y: E::Unit, w: W) -> Min { - Min::Y(y, w) + Min::Y(y, w) } fn min_xy > (x: E::Unit, y: E::Unit, w: W) -> Min { Min::XY(x, y, w) diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index 2f9b5f1c..9bdda0f8 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -259,7 +259,7 @@ impl Tui { //selected.wrap(TransportFocus::Bpm, &Outset::X(1u16, { //row! { //"BPM ", - //format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0) + //format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0) //} //})), //selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! { diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index ff689c59..8347f172 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -1288,7 +1288,7 @@ impl ArrangerSelection { match self { Clip(_, s) => Some(*s), Scene(s) => Some(*s), - _ => None + _ => None } } } diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index 795b5f58..c626e38e 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -134,7 +134,7 @@ impl AddSampleModal { voices: voices.clone(), _search: None }) - } + } fn rescan (&mut self) -> Usually<()> { scan(&self.dir).map(|(subdirs, files)|{ self.subdirs = subdirs; diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index bf260365..eede64c2 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -233,7 +233,7 @@ impl std::fmt::Debug for PhraseEditorModel { pub struct PhraseEditStatus<'a>(pub &'a PhraseEditorModel); render!(|self:PhraseEditStatus<'a>|row!(|add|{ let (color, name, length, looped) = if let Some(phrase) = self.0.phrase().as_ref().map(|p|p.read().unwrap()) { - (phrase.color, phrase.name.clone(), phrase.length, phrase.loop_on) + (phrase.color, phrase.name.clone(), phrase.length, phrase.looped) } else { (ItemPalette::from(TuiTheme::g(64)), String::new(), 0, false) }; diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index b80ce7da..dd0f60ca 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -242,7 +242,7 @@ render!(|self: PhraseListView<'a>|{ let mut row2 = format!(" {name}"); if let Some(PhraseListMode::Rename(phrase, _)) = mode { if i == *phrase { - row2 = format!("{row2}▄"); + row2 = format!("{row2}▄"); } }; row2 @@ -310,7 +310,7 @@ impl PhraseSelector { (time, name.clone(), color) } else if let Some((_, Some(phrase))) = state.play_phrase() { let phrase = phrase.read().unwrap(); - if phrase.loop_on { + if phrase.looped { (" ".into(), phrase.name.clone(), phrase.color.clone()) } else { (" ".into(), " ".into(), TuiTheme::g(64).into()) From 5c630cc51b3513d4da8b5031e2172d9472ed6973 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 17 Dec 2024 01:03:37 +0100 Subject: [PATCH 002/905] wip: align timeline to notes area --- crates/tek/src/tui/piano_horizontal.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index a89483ac..38ac132f 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -84,11 +84,23 @@ render!(|self: PianoHorizontal|{ }); pub struct PianoHorizontalTimeline<'a>(&'a PianoHorizontal); -render!(|self: PianoHorizontalTimeline<'a>|Tui::fg_bg( - self.0.color.lightest.rgb, - self.0.color.darkest.rgb, - format!("{}*{}", self.0.time_start(), self.0.time_zoom()).as_str() -)); +render!(|self: PianoHorizontalTimeline<'a>|render(|to|{ + let [x, y, w, h] = to.area(); + let style = Some(Style::default().dim()); + let length = self.0.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { + let t = area_x as usize * self.0.time_zoom(); + if t < length { + to.blit(&"|", screen_x, y, style); + } + } + Ok(()) +})); + //Tui::fg_bg( + //self.0.color.lightest.rgb, + //self.0.color.darkest.rgb, + //format!("{}*{}", self.0.time_start(), self.0.time_zoom()).as_str() +//)); pub struct PianoHorizontalKeys<'a>(&'a PianoHorizontal); render!(|self: PianoHorizontalKeys<'a>|render(|to|Ok({ From a352141dde2741777eababd78279cd68e5743838 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 17 Dec 2024 01:57:22 +0100 Subject: [PATCH 003/905] add handle! macro and enable groovebox --- Justfile | 19 +++ crates/tek/Cargo.toml | 4 + crates/tek/src/cli/cli_groovebox.rs | 25 ++++ crates/tek/src/core/input.rs | 10 ++ crates/tek/src/lib.rs | 6 +- crates/tek/src/tui/_todo_tui_mixer.rs | 155 +++++++++++---------- crates/tek/src/tui/_todo_tui_plugin.rs | 121 +++++++++-------- crates/tek/src/tui/app_arranger.rs | 180 ++++++++++++------------- crates/tek/src/tui/app_groovebox.rs | 6 +- crates/tek/src/tui/app_sampler.rs | 66 +++++---- crates/tek/src/tui/app_sequencer.rs | 51 +++---- crates/tek/src/tui/app_transport.rs | 7 +- crates/tek/src/tui/piano_horizontal.rs | 9 +- 13 files changed, 350 insertions(+), 309 deletions(-) diff --git a/Justfile b/Justfile index 19f8e663..611f5fe2 100644 --- a/Justfile +++ b/Justfile @@ -1,9 +1,11 @@ default: just -l + status: cargo c cloc --by-file src/ git status + push: git push -u codeberg main git push -u origin main @@ -16,18 +18,35 @@ fpush: ftpush: git push --tags -fu codeberg git push --tags -fu origin + transport: reset cargo run --bin tek_transport +transport-release: + reset + cargo run --release --bin tek_transport + arranger: reset cargo run --bin tek_arranger +arranger-release: + reset + cargo run --release --bin tek_arranger + +groovebox: + reset + cargo run --bin tek_groovebox +groovebox-release: + reset + cargo run --release --bin tek_groovebox + sequencer: reset cargo run --bin tek_sequencer sequencer-release: reset cargo run --release --bin tek_sequencer + mixer: reset cargo run --bin tek_mixer diff --git a/crates/tek/Cargo.toml b/crates/tek/Cargo.toml index 77967f73..349ca090 100644 --- a/crates/tek/Cargo.toml +++ b/crates/tek/Cargo.toml @@ -39,6 +39,10 @@ path = "src/cli/cli_arranger.rs" name = "tek_sequencer" path = "src/cli/cli_sequencer.rs" +[[bin]] +name = "tek_groovebox" +path = "src/cli/cli_groovebox.rs" + [[bin]] name = "tek_transport" path = "src/cli/cli_transport.rs" diff --git a/crates/tek/src/cli/cli_groovebox.rs b/crates/tek/src/cli/cli_groovebox.rs index e69de29b..77bac1bc 100644 --- a/crates/tek/src/cli/cli_groovebox.rs +++ b/crates/tek/src/cli/cli_groovebox.rs @@ -0,0 +1,25 @@ +include!("../lib.rs"); +pub fn main () -> Usually<()> { + GrooveboxCli::parse().run() +} + +#[derive(Debug, Parser)] +#[command(version, about, long_about = None)] +pub struct GrooveboxCli; + +impl GrooveboxCli { + fn run (&self) -> Usually<()> { + Tui::run(JackClient::new("tek_groovebox")?.activate_with(|jack|{ + let midi_in_1 = jack.read().unwrap().register_port("in1", MidiIn::default())?; + let midi_out = jack.read().unwrap().register_port("out", MidiOut::default())?; + let midi_in_2 = jack.read().unwrap().register_port("in2", MidiIn::default())?; + let audio_in_1 = jack.read().unwrap().register_port("inL", AudioIn::default())?; + let audio_in_2 = jack.read().unwrap().register_port("inR", AudioIn::default())?; + let audio_out_1 = jack.read().unwrap().register_port("out1", AudioOut::default())?; + let audio_out_2 = jack.read().unwrap().register_port("out2", AudioOut::default())?; + let mut app = GrooveboxTui::try_from(jack)?; + Ok(app) + })?)?; + Ok(()) + } +} diff --git a/crates/tek/src/core/input.rs b/crates/tek/src/core/input.rs index e9388793..19c42b3b 100644 --- a/crates/tek/src/core/input.rs +++ b/crates/tek/src/core/input.rs @@ -17,6 +17,16 @@ pub trait Handle: Send + Sync { fn handle (&mut self, context: &E::Input) -> Perhaps; } +#[macro_export] macro_rules! handle { + (<$E:ty>|$self:ident:$Struct:ty,$input:ident|$handler:expr) => { + impl Handle<$E> for $Struct { + fn handle (&mut $self, $input: &<$E as Engine>::Input) -> Perhaps<<$E as Engine>::Handled> { + $handler + } + } + } +} + impl> Handle for &mut H { fn handle (&mut self, context: &E::Input) -> Perhaps { (*self).handle(context) diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs index f13af271..84ad431d 100644 --- a/crates/tek/src/lib.rs +++ b/crates/tek/src/lib.rs @@ -18,10 +18,10 @@ pub(crate) use ratatui::{ pub(crate) use jack; pub(crate) use jack::{ - Client, ProcessScope, Control, CycleTimes, - Port, PortSpec, MidiIn, MidiOut, AudioOut, Unowned, - Transport, TransportState, MidiIter, RawMidi, contrib::ClosureProcessHandler, + Client, ProcessScope, Control, CycleTimes, + Port, PortSpec, MidiIn, MidiOut, AudioIn, AudioOut, Unowned, + Transport, TransportState, MidiIter, RawMidi, }; pub(crate) use midly; diff --git a/crates/tek/src/tui/_todo_tui_mixer.rs b/crates/tek/src/tui/_todo_tui_mixer.rs index c8203770..111d5cb2 100644 --- a/crates/tek/src/tui/_todo_tui_mixer.rs +++ b/crates/tek/src/tui/_todo_tui_mixer.rs @@ -164,88 +164,85 @@ impl Content for Track { } } -impl Handle for Mixer { - fn handle (&mut self, engine: &TuiInput) -> Perhaps { - if let TuiEvent::Input(crossterm::event::Event::Key(event)) = engine.event() { +handle!(|self:Mixer,engine|{ + if let TuiEvent::Input(crossterm::event::Event::Key(event)) = engine.event() { - match event.code { - //KeyCode::Char('c') => { - //if event.modifiers == KeyModifiers::CONTROL { - //self.exit(); - //} - //}, - KeyCode::Down => { - self.selected_track = (self.selected_track + 1) % self.tracks.len(); - println!("{}", self.selected_track); - return Ok(Some(true)) - }, - KeyCode::Up => { - if self.selected_track == 0 { - self.selected_track = self.tracks.len() - 1; - } else { - self.selected_track -= 1; - } - println!("{}", self.selected_track); - return Ok(Some(true)) - }, - KeyCode::Left => { - if self.selected_column == 0 { - self.selected_column = 6 - } else { - self.selected_column -= 1; - } - return Ok(Some(true)) - }, - KeyCode::Right => { - if self.selected_column == 6 { - self.selected_column = 0 - } else { - self.selected_column += 1; - } - return Ok(Some(true)) - }, - _ => { - println!("\n{event:?}"); + match event.code { + //KeyCode::Char('c') => { + //if event.modifiers == KeyModifiers::CONTROL { + //self.exit(); + //} + //}, + KeyCode::Down => { + self.selected_track = (self.selected_track + 1) % self.tracks.len(); + println!("{}", self.selected_track); + return Ok(Some(true)) + }, + KeyCode::Up => { + if self.selected_track == 0 { + self.selected_track = self.tracks.len() - 1; + } else { + self.selected_track -= 1; } + println!("{}", self.selected_track); + return Ok(Some(true)) + }, + KeyCode::Left => { + if self.selected_column == 0 { + self.selected_column = 6 + } else { + self.selected_column -= 1; + } + return Ok(Some(true)) + }, + KeyCode::Right => { + if self.selected_column == 6 { + self.selected_column = 0 + } else { + self.selected_column += 1; + } + return Ok(Some(true)) + }, + _ => { + println!("\n{event:?}"); } + } - } - Ok(None) } -} -impl Handle for Track { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - match from.event() { - //, NONE, "chain_cursor_up", "move cursor up", || { - key!(KeyCode::Up) => { - Ok(Some(true)) - }, - // , NONE, "chain_cursor_down", "move cursor down", || { - key!(KeyCode::Down) => { - Ok(Some(true)) - }, - // Left, NONE, "chain_cursor_left", "move cursor left", || { - key!(KeyCode::Left) => { - //if let Some(track) = app.arranger.track_mut() { - //track.device = track.device.saturating_sub(1); - //return Ok(true) - //} - Ok(Some(true)) - }, - // , NONE, "chain_cursor_right", "move cursor right", || { - key!(KeyCode::Right) => { - //if let Some(track) = app.arranger.track_mut() { - //track.device = (track.device + 1).min(track.devices.len().saturating_sub(1)); - //return Ok(true) - //} - Ok(Some(true)) - }, - // , NONE, "chain_mode_switch", "switch the display mode", || { - key!(KeyCode::Char('`')) => { - //app.chain_mode = !app.chain_mode; - Ok(Some(true)) - }, - _ => Ok(None) - } + Ok(None) +}); + +handle!(|self:Track,from|{ + match from.event() { + //, NONE, "chain_cursor_up", "move cursor up", || { + key!(KeyCode::Up) => { + Ok(Some(true)) + }, + // , NONE, "chain_cursor_down", "move cursor down", || { + key!(KeyCode::Down) => { + Ok(Some(true)) + }, + // Left, NONE, "chain_cursor_left", "move cursor left", || { + key!(KeyCode::Left) => { + //if let Some(track) = app.arranger.track_mut() { + //track.device = track.device.saturating_sub(1); + //return Ok(true) + //} + Ok(Some(true)) + }, + // , NONE, "chain_cursor_right", "move cursor right", || { + key!(KeyCode::Right) => { + //if let Some(track) = app.arranger.track_mut() { + //track.device = (track.device + 1).min(track.devices.len().saturating_sub(1)); + //return Ok(true) + //} + Ok(Some(true)) + }, + // , NONE, "chain_mode_switch", "switch the display mode", || { + key!(KeyCode::Char('`')) => { + //app.chain_mode = !app.chain_mode; + Ok(Some(true)) + }, + _ => Ok(None) } -} +}); diff --git a/crates/tek/src/tui/_todo_tui_plugin.rs b/crates/tek/src/tui/_todo_tui_plugin.rs index 54cf0704..8f4eddfd 100644 --- a/crates/tek/src/tui/_todo_tui_plugin.rs +++ b/crates/tek/src/tui/_todo_tui_plugin.rs @@ -83,66 +83,65 @@ fn draw_header (state: &Plugin, to: &mut TuiOutput, x: u16, y: u16, w: u1 Ok(Rect { x, y, width: w, height: 1 }) } -impl Handle for Plugin { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - match from.event() { - key!(KeyCode::Up) => { - self.selected = self.selected.saturating_sub(1); - Ok(Some(true)) - }, - key!(KeyCode::Down) => { - self.selected = (self.selected + 1).min(match &self.plugin { - Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, - _ => unimplemented!() - }); - Ok(Some(true)) - }, - key!(KeyCode::PageUp) => { - self.selected = self.selected.saturating_sub(8); - Ok(Some(true)) - }, - key!(KeyCode::PageDown) => { - self.selected = (self.selected + 10).min(match &self.plugin { - Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, - _ => unimplemented!() - }); - Ok(Some(true)) - }, - key!(KeyCode::Char(',')) => { - match self.plugin.as_mut() { - Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { - let index = port_list[self.selected].index; - if let Some(value) = instance.control_input(index) { - instance.set_control_input(index, value - 0.01); - } - }, - _ => {} - } - Ok(Some(true)) - }, - key!(KeyCode::Char('.')) => { - match self.plugin.as_mut() { - Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { - let index = port_list[self.selected].index; - if let Some(value) = instance.control_input(index) { - instance.set_control_input(index, value + 0.01); - } - }, - _ => {} - } - Ok(Some(true)) - }, - key!(KeyCode::Char('g')) => { - match self.plugin { - Some(PluginKind::LV2(ref mut plugin)) => { - plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?); - }, - Some(_) => unreachable!(), - None => {} - } - Ok(Some(true)) - }, - _ => Ok(None) - } +handle!(|self:Plugin,from|{ + match from.event() { + key!(KeyCode::Up) => { + self.selected = self.selected.saturating_sub(1); + Ok(Some(true)) + }, + key!(KeyCode::Down) => { + self.selected = (self.selected + 1).min(match &self.plugin { + Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, + _ => unimplemented!() + }); + Ok(Some(true)) + }, + key!(KeyCode::PageUp) => { + self.selected = self.selected.saturating_sub(8); + Ok(Some(true)) + }, + key!(KeyCode::PageDown) => { + self.selected = (self.selected + 10).min(match &self.plugin { + Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, + _ => unimplemented!() + }); + Ok(Some(true)) + }, + key!(KeyCode::Char(',')) => { + match self.plugin.as_mut() { + Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { + let index = port_list[self.selected].index; + if let Some(value) = instance.control_input(index) { + instance.set_control_input(index, value - 0.01); + } + }, + _ => {} + } + Ok(Some(true)) + }, + key!(KeyCode::Char('.')) => { + match self.plugin.as_mut() { + Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { + let index = port_list[self.selected].index; + if let Some(value) = instance.control_input(index) { + instance.set_control_input(index, value + 0.01); + } + }, + _ => {} + } + Ok(Some(true)) + }, + key!(KeyCode::Char('g')) => { + match self.plugin { + Some(PluginKind::LV2(ref mut plugin)) => { + plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?); + }, + Some(_) => unreachable!(), + None => {} + } + Ok(Some(true)) + }, + _ => Ok(None) } +}); } diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 8347f172..858647b4 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -56,9 +56,94 @@ pub struct ArrangerTui { pub perf: PerfModel, } -impl Handle for ArrangerTui { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - ArrangerCommand::execute_with_state(self, i) +has_clock!(|self:ArrangerTui|&self.clock); +has_phrases!(|self:ArrangerTui|self.phrases.phrases); +has_editor!(|self:ArrangerTui|self.editor); +handle!(|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input)); +render!(|self: ArrangerTui|{ + let arranger_focused = self.arranger_focused(); + let transport_focused = if let ArrangerFocus::Transport(_) = self.focus.inner() { + true + } else { + false + }; + let transport = TransportView::from((self, None, transport_focused)); + let with_transport = move|x|col!([transport, x]); + let border = Lozenge(Style::default() + .bg(TuiTheme::border_bg()) + .fg(TuiTheme::border_fg(arranger_focused))); + let arranger = move||border.wrap(Tui::grow_y(1, lay!(|add|{ + match self.mode { + ArrangerMode::Horizontal => add(&arranger_content_horizontal(self))?, + ArrangerMode::Vertical(factor) => add(&arranger_content_vertical(self, factor))? + }; + add(&self.size) + }))); + with_transport(col!([ + Tui::fixed_y(self.splits[0], lay!([ + arranger(), + Tui::push_x(1, Tui::fg( + TuiTheme::title_fg(arranger_focused), + format!("[{}] Arranger", if self.entered { + "■" + } else { + " " + }) + )) + ])), + Split::right(false, self.splits[1], PhraseListView(&self.phrases), &self.editor), + ])) +}); +audio!(|self: ArrangerTui, client, scope|{ + // Start profiling cycle + let t0 = self.perf.get_t0(); + // Update transport clock + if ClockAudio(self).process(client, scope) == Control::Quit { + return Control::Quit + } + // Update MIDI sequencers + let tracks = &mut self.tracks; + let note_buf = &mut self.note_buf; + let midi_buf = &mut self.midi_buf; + if TracksAudio(tracks, note_buf, midi_buf, Default::default()) + .process(client, scope) == Control::Quit { + return Control::Quit + } + // FIXME: one of these per playing track + //self.now.set(0.); + //if let ArrangerSelection::Clip(t, s) = self.selected { + //let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t)); + //if let Some(Some(Some(phrase))) = phrase { + //if let Some(track) = self.tracks().get(t) { + //if let Some((ref started_at, Some(ref playing))) = track.player.play_phrase { + //let phrase = phrase.read().unwrap(); + //if *playing.read().unwrap() == *phrase { + //let pulse = self.current().pulse.get(); + //let start = started_at.pulse.get(); + //let now = (pulse - start) % phrase.length as f64; + //self.now.set(now); + //} + //} + //} + //} + //} + // End profiling cycle + self.perf.update(t0, scope); + return Control::Continue +}); + +impl HasPhraseList for ArrangerTui { + fn phrases_focused (&self) -> bool { + self.focused() == ArrangerFocus::Phrases + } + fn phrases_entered (&self) -> bool { + self.entered() && self.phrases_focused() + } + fn phrases_mode (&self) -> &Option { + &self.phrases.mode + } + fn phrase_index (&self) -> usize { + self.phrases.phrase.load(Ordering::Relaxed) } } @@ -320,97 +405,8 @@ impl TransportControl for ArrangerTui { } } } -has_clock!(|self:ArrangerTui|&self.clock); has_clock!(|self:ArrangerTrack|self.player.clock()); -has_phrases!(|self:ArrangerTui|self.phrases.phrases); -has_editor!(|self:ArrangerTui|self.editor); has_player!(|self:ArrangerTrack|self.player); -render!(|self: ArrangerTui|{ - let arranger_focused = self.arranger_focused(); - let transport_focused = if let ArrangerFocus::Transport(_) = self.focus.inner() { - true - } else { - false - }; - let transport = TransportView::from((self, None, transport_focused)); - let with_transport = move|x|col!([transport, x]); - let border = Lozenge(Style::default() - .bg(TuiTheme::border_bg()) - .fg(TuiTheme::border_fg(arranger_focused))); - let arranger = move||border.wrap(Tui::grow_y(1, lay!(|add|{ - match self.mode { - ArrangerMode::Horizontal => add(&arranger_content_horizontal(self))?, - ArrangerMode::Vertical(factor) => add(&arranger_content_vertical(self, factor))? - }; - add(&self.size) - }))); - with_transport(col!([ - Tui::fixed_y(self.splits[0], lay!([ - arranger(), - Tui::push_x(1, Tui::fg( - TuiTheme::title_fg(arranger_focused), - format!("[{}] Arranger", if self.entered { - "■" - } else { - " " - }) - )) - ])), - Split::right(false, self.splits[1], PhraseListView(&self.phrases), &self.editor), - ])) -}); -audio!(|self: ArrangerTui, client, scope|{ - // Start profiling cycle - let t0 = self.perf.get_t0(); - // Update transport clock - if ClockAudio(self).process(client, scope) == Control::Quit { - return Control::Quit - } - // Update MIDI sequencers - let tracks = &mut self.tracks; - let note_buf = &mut self.note_buf; - let midi_buf = &mut self.midi_buf; - if TracksAudio(tracks, note_buf, midi_buf, Default::default()) - .process(client, scope) == Control::Quit { - return Control::Quit - } - // FIXME: one of these per playing track - //self.now.set(0.); - //if let ArrangerSelection::Clip(t, s) = self.selected { - //let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t)); - //if let Some(Some(Some(phrase))) = phrase { - //if let Some(track) = self.tracks().get(t) { - //if let Some((ref started_at, Some(ref playing))) = track.player.play_phrase { - //let phrase = phrase.read().unwrap(); - //if *playing.read().unwrap() == *phrase { - //let pulse = self.current().pulse.get(); - //let start = started_at.pulse.get(); - //let now = (pulse - start) % phrase.length as f64; - //self.now.set(now); - //} - //} - //} - //} - //} - // End profiling cycle - self.perf.update(t0, scope); - return Control::Continue -}); - -impl HasPhraseList for ArrangerTui { - fn phrases_focused (&self) -> bool { - self.focused() == ArrangerFocus::Phrases - } - fn phrases_entered (&self) -> bool { - self.entered() && self.phrases_focused() - } - fn phrases_mode (&self) -> &Option { - &self.phrases.mode - } - fn phrase_index (&self) -> usize { - self.phrases.phrase.load(Ordering::Relaxed) - } -} /// Sections in the arranger app that may be focused #[derive(Copy, Clone, PartialEq, Eq, Debug)] diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index efd4f1fb..43a45923 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -12,7 +12,7 @@ impl TryFrom<&Arc>> for GrooveboxTui { } } -struct GrooveboxTui { +pub struct GrooveboxTui { pub sequencer: SequencerTui, pub sampler: SamplerTui, pub focus: GrooveboxFocus, @@ -30,3 +30,7 @@ pub enum GrooveboxFocus { /// The sample player is focused Sampler } + +render!(|self:GrooveboxTui|"are we groovy yet?"); +audio!(|self:GrooveboxTui,_client,_process|Control::Continue); +handle!(|self:GrooveboxTui,input|Ok(None)); diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index c626e38e..9501d125 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -221,42 +221,40 @@ fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { todo!(); } -impl Handle for SamplerTui { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - let cursor = &mut self.cursor; - let unmapped = &mut self.state.unmapped; - let mapped = &self.state.mapped; - let voices = &self.state.voices; - match from.event() { - key_pat!(KeyCode::Up) => cursor.0 = if cursor.0 == 0 { - mapped.len() + unmapped.len() - 1 - } else { - cursor.0 - 1 - }, - key_pat!(KeyCode::Down) => { - cursor.0 = (cursor.0 + 1) % (mapped.len() + unmapped.len()); - }, - key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { - voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); - }, - key_pat!(KeyCode::Char('a')) => { - let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); - unmapped.push(sample); - }, - key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); - }, - key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { - self.editing = Some(sample.clone()); - }, - _ => { - return Ok(None) - } +handle!(|self:SamplerTui,from|{ + let cursor = &mut self.cursor; + let unmapped = &mut self.state.unmapped; + let mapped = &self.state.mapped; + let voices = &self.state.voices; + match from.event() { + key_pat!(KeyCode::Up) => cursor.0 = if cursor.0 == 0 { + mapped.len() + unmapped.len() - 1 + } else { + cursor.0 - 1 + }, + key_pat!(KeyCode::Down) => { + cursor.0 = (cursor.0 + 1) % (mapped.len() + unmapped.len()); + }, + key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { + voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); + }, + key_pat!(KeyCode::Char('a')) => { + let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + unmapped.push(sample); + }, + key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { + *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + }, + key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { + self.editing = Some(sample.clone()); + }, + _ => { + return Ok(None) } - Ok(Some(true)) } -} + Ok(Some(true)) +}); fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { let (mut subdirs, mut files) = std::fs::read_dir(dir)? diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 94679b86..a17aa161 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -144,24 +144,11 @@ impl InputToCommand for SequencerCommand { } } -audio!(|self:SequencerTui, 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 - } - // Update MIDI sequencer - if Control::Quit == PlayerAudio( - &mut self.player, &mut self.note_buf, &mut self.midi_buf - ).process(client, scope) { - return Control::Quit - } - // End profiling cycle - self.perf.update(t0, scope); - Control::Continue -}); - +has_size!(|self:SequencerTui|&self.size); +has_clock!(|self:SequencerTui|&self.clock); +has_phrases!(|self:SequencerTui|self.phrases.phrases); +has_editor!(|self:SequencerTui|self.editor); +handle!(|self:SequencerTui,i|SequencerCommand::execute_with_state(self, i)); render!(|self: SequencerTui|{ let w = self.size.w(); let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; @@ -181,11 +168,23 @@ render!(|self: SequencerTui|{ ]), transport]); with_size(with_status(col!([ toolbar, editor, ]))) }); - -has_size!(|self:SequencerTui|&self.size); -has_clock!(|self:SequencerTui|&self.clock); -has_phrases!(|self:SequencerTui|self.phrases.phrases); -has_editor!(|self:SequencerTui|self.editor); +audio!(|self:SequencerTui, 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 + } + // Update MIDI sequencer + if Control::Quit == PlayerAudio( + &mut self.player, &mut self.note_buf, &mut self.midi_buf + ).process(client, scope) { + return Control::Quit + } + // End profiling cycle + self.perf.update(t0, scope); + Control::Continue +}); impl HasPhraseList for SequencerTui { fn phrases_focused (&self) -> bool { @@ -202,12 +201,6 @@ impl HasPhraseList for SequencerTui { } } -impl Handle for SequencerTui { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - SequencerCommand::execute_with_state(self, i) - } -} - /// Status bar for sequencer app #[derive(Clone)] pub struct SequencerStatusBar { diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index fe8a4316..bc15dddf 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -39,6 +39,7 @@ impl std::fmt::Debug for TransportTui { has_clock!(|self:TransportTui|&self.clock); audio!(|self:TransportTui,client,scope|ClockAudio(self).process(client, scope)); +handle!(|self:TransportTui,from|TransportCommand::execute_with_state(self, from)); render!(|self: TransportTui|TransportView::from((self, None, true))); pub struct TransportView { @@ -201,12 +202,6 @@ impl StatusBar for TransportStatusBar { render!(|self: TransportStatusBar|"todo"); -impl Handle for TransportTui { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - TransportCommand::execute_with_state(self, from) - } -} - pub trait TransportControl: HasClock + { fn transport_focused (&self) -> Option; } diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index 38ac132f..a28f9877 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -239,10 +239,11 @@ impl PianoHorizontal { for (x, time_start) in (0..phrase.length).step_by(zoom).enumerate() { for (y, note) in (0..127).rev().enumerate() { - let cell = buf.get_mut(x, note).unwrap(); - if notes_on[note] { - cell.set_char('▂'); - cell.set_style(style); + if let Some(cell) = buf.get_mut(x, note) { + if notes_on[note] { + cell.set_char('▂'); + cell.set_style(style); + } } } From c68562178881a768b6fd10bcbf026ca023a21c22 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 17 Dec 2024 11:59:22 +0100 Subject: [PATCH 004/905] implement Bsp::N --- crates/tek/src/layout/bsp.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/tek/src/layout/bsp.rs b/crates/tek/src/layout/bsp.rs index af3ac295..a8bffc3d 100644 --- a/crates/tek/src/layout/bsp.rs +++ b/crates/tek/src/layout/bsp.rs @@ -52,6 +52,11 @@ impl, Y: Render> Render for Bsp { let b = b.min_size(to)?.unwrap_or([0.into(), 0.into()].into()); [a.w() + b.w(), a.h().max(b.h())].into() }, + Self::N(a, b) => { + let a = a.min_size(to)?.unwrap_or([0.into(), 0.into()].into()); + let b = b.min_size(to)?.unwrap_or([0.into(), 0.into()].into()); + [a.w().max(b.w()), a.h() + b.h()].into() + }, _ => todo!() })) } @@ -85,6 +90,15 @@ impl, Y: Render> Render for Bsp { to.render_in(to.area().push_x(s_x).into(), a)?; to.render_in(to.area().shrink_x(s_x).into(), b)?; }, + Self::N(a, b) => { + let n = [0.into(), 0.into()].into(); + let s = to.area().wh().into(); + let s_a = a.min_size(s)?.unwrap_or(n); + let s_b = b.min_size(s)?.unwrap_or(n); + let s_y = (to.area().h() - s_a.h()).into(); + to.render_in(to.area().push_y(s_y).into(), a)?; + to.render_in(to.area().shrink_y(s_y).into(), b)?; + }, _ => todo!() }) } From bd7e1d16d65b0530df4d235e50501fef520ffcb7 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 17 Dec 2024 11:59:42 +0100 Subject: [PATCH 005/905] simplify groovebox module --- crates/tek/src/tui/app_groovebox.rs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index 43a45923..e0ea179b 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -7,7 +7,7 @@ impl TryFrom<&Arc>> for GrooveboxTui { Ok(Self { sequencer: SequencerTui::try_from(jack)?, sampler: SamplerTui::try_from(jack)?, - focus: GrooveboxFocus::Sampler, + split: 20 }) } } @@ -15,22 +15,9 @@ impl TryFrom<&Arc>> for GrooveboxTui { pub struct GrooveboxTui { pub sequencer: SequencerTui, pub sampler: SamplerTui, - pub focus: GrooveboxFocus, + pub split: u16, } -/// Sections that may be focused -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum GrooveboxFocus { - /// The transport (toolbar) is focused - Transport(TransportFocus), - /// The phrase list (pool) is focused - PhraseList, - /// The phrase editor (sequencer) is focused - PhraseEditor, - /// The sample player is focused - Sampler -} - -render!(|self:GrooveboxTui|"are we groovy yet?"); +render!(|self:GrooveboxTui|Bsp::n(Tui::fixed_y(self.split, &self.sampler), &self.sequencer)); audio!(|self:GrooveboxTui,_client,_process|Control::Continue); handle!(|self:GrooveboxTui,input|Ok(None)); From 471d5bc0d3a9dc23502a644296ca7355fcb1e1a9 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 17 Dec 2024 17:43:48 +0100 Subject: [PATCH 006/905] fix bsp north and stack sampler/sequencer --- crates/tek/src/layout/bsp.rs | 42 +++++++++++++---------------- crates/tek/src/tui/app_groovebox.rs | 10 ++++--- crates/tek/src/tui/app_sampler.rs | 18 +++++++++---- crates/tek/src/tui/app_sequencer.rs | 12 ++++----- 4 files changed, 43 insertions(+), 39 deletions(-) diff --git a/crates/tek/src/layout/bsp.rs b/crates/tek/src/layout/bsp.rs index a8bffc3d..083761e1 100644 --- a/crates/tek/src/layout/bsp.rs +++ b/crates/tek/src/layout/bsp.rs @@ -61,43 +61,37 @@ impl, Y: Render> Render for Bsp { })) } fn render (&self, to: &mut E::Output) -> Usually<()> { + let n = [0.into(), 0.into()].into(); + let s = to.area().wh().into(); Ok(match self { Self::Null(_) => {}, Self::S(a, b) => { - let n = [0.into(), 0.into()].into(); - let s = to.area().wh().into(); let s_a = a.min_size(s)?.unwrap_or(n); - let s_b = b.min_size(s)?.unwrap_or(n); - let s_y = s_a.h().into(); - to.render_in(to.area().clip_h(s_y).into(), a)?; - to.render_in(to.area().push_y(s_y).shrink_y(s_y).into(), b)?; + //let s_b = b.min_size(s)?.unwrap_or(n); + let h = s_a.h().into(); + to.render_in(to.area().clip_h(h).into(), a)?; + to.render_in(to.area().push_y(h).shrink_y(h).into(), b)?; }, Self::E(a, b) => { - let n = [0.into(), 0.into()].into(); - let s = to.area().wh().into(); let s_a = a.min_size(s)?.unwrap_or(n); - let s_b = b.min_size(s)?.unwrap_or(n); - let s_x = s_a.w().into(); - to.render_in(to.area().clip_w(s_x).into(), a)?; - to.render_in(to.area().push_x(s_x).shrink_x(s_x).into(), b)?; + //let s_b = b.min_size(s)?.unwrap_or(n); + let w = s_a.w().into(); + to.render_in(to.area().clip_w(w).into(), a)?; + to.render_in(to.area().push_x(w).shrink_x(w).into(), b)?; }, Self::W(a, b) => { - let n = [0.into(), 0.into()].into(); - let s = to.area().wh().into(); let s_a = a.min_size(s)?.unwrap_or(n); - let s_b = b.min_size(s)?.unwrap_or(n); - let s_x = (to.area().w() - s_a.w()).into(); - to.render_in(to.area().push_x(s_x).into(), a)?; - to.render_in(to.area().shrink_x(s_x).into(), b)?; + //let s_b = b.min_size(s)?.unwrap_or(n); + let w = (to.area().w() - s_a.w()).into(); + to.render_in(to.area().push_x(w).into(), a)?; + to.render_in(to.area().shrink_x(w).into(), b)?; }, Self::N(a, b) => { - let n = [0.into(), 0.into()].into(); - let s = to.area().wh().into(); let s_a = a.min_size(s)?.unwrap_or(n); - let s_b = b.min_size(s)?.unwrap_or(n); - let s_y = (to.area().h() - s_a.h()).into(); - to.render_in(to.area().push_y(s_y).into(), a)?; - to.render_in(to.area().shrink_y(s_y).into(), b)?; + //let s_b = b.min_size(s)?.unwrap_or(n); + let h = to.area().h() - s_a.h(); + to.render_in(to.area().push_y(h).into(), a)?; + to.render_in(to.area().shrink_y(h).into(), b)?; }, _ => todo!() }) diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index e0ea179b..7b44bfc5 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -4,10 +4,12 @@ use super::*; impl TryFrom<&Arc>> for GrooveboxTui { type Error = Box; fn try_from (jack: &Arc>) -> Usually { + let mut sequencer = SequencerTui::try_from(jack)?; + sequencer.status = false; Ok(Self { - sequencer: SequencerTui::try_from(jack)?, - sampler: SamplerTui::try_from(jack)?, - split: 20 + sequencer, + sampler: SamplerTui::try_from(jack)?, + split: 16 }) } } @@ -18,6 +20,6 @@ pub struct GrooveboxTui { pub split: u16, } -render!(|self:GrooveboxTui|Bsp::n(Tui::fixed_y(self.split, &self.sampler), &self.sequencer)); +render!(|self:GrooveboxTui|Bsp::s(Tui::fixed_y(self.split, &self.sequencer), &self.sampler)); audio!(|self:GrooveboxTui,_client,_process|Control::Continue); handle!(|self:GrooveboxTui,input|Ok(None)); diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index 9501d125..bef748fc 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -52,10 +52,18 @@ pub enum SamplerFocus { audio!(|self: SamplerTui, _client, _scope|Control::Continue); render!(|self: SamplerTui|render(|to|{ - let [x, y, _, height] = to.area(); + let [x, y, w, h] = to.area(); + let style = Some(Style::default().fg(Color::Green)); + to.blit(&"🭚", x, y, style); + to.blit(&"🭥", x + w.saturating_sub(1), y, style); + to.blit(&"🬿", x, y + h.saturating_sub(1), style); + to.blit(&"🭊", x + w.saturating_sub(1), y + h.saturating_sub(1), style); + let style = Style::default().gray(); - let title = format!(" {} ({})", self.state.name, self.state.voices.read().unwrap().len()); + let voice = self.state.voices.read().unwrap().len(); + let title = format!(" {} ({voice})", self.state.name); to.blit(&title, x+1, y, Some(style.white().bold().not_dim())); + let mut width = title.len() + 2; let mut y1 = 1; let mut j = 0; @@ -63,7 +71,7 @@ render!(|self: SamplerTui|render(|to|{ .map(|(note, sample)|(Some(note), sample)) .chain(self.state.unmapped.iter().map(|sample|(None, sample))) { - if y1 >= height { + if y1 >= h { break } let active = j == self.cursor.0; @@ -73,8 +81,8 @@ render!(|self: SamplerTui|render(|to|{ y1 = y1 + 1; j = j + 1; } - let height = ((2 + y1) as u16).min(height); - //Ok(Some([x, y, (width as u16).min(to.area().w()), height])) + let h = ((2 + y1) as u16).min(h); + //Ok(Some([x, y, (width as u16).min(to.area().w()), h])) Ok(()) })); diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index a17aa161..029d79fa 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -10,11 +10,8 @@ impl TryFrom<&Arc>> for SequencerTui { fn try_from (jack: &Arc>) -> Usually { let clock = ClockModel::from(jack); let phrase = Arc::new(RwLock::new(Phrase::new( - "New", - true, - 4 * clock.timebase.ppq.get() as usize, - None, - Some(ItemColor::random().into()) + "New", true, 4 * clock.timebase.ppq.get() as usize, + None, Some(ItemColor::random().into()) ))); Ok(Self { _jack: jack.clone(), @@ -27,6 +24,7 @@ impl TryFrom<&Arc>> for SequencerTui { note_buf: vec![], perf: PerfModel::default(), show_pool: true, + status: true, }) } @@ -41,6 +39,7 @@ pub struct SequencerTui { pub(crate) editor: PhraseEditorModel, pub(crate) size: Measure, pub(crate) show_pool: bool, + pub(crate) status: bool, pub(crate) note_buf: Vec, pub(crate) midi_buf: Vec>>, pub(crate) perf: PerfModel, @@ -155,7 +154,8 @@ render!(|self: SequencerTui|{ let pool_w = if self.show_pool { phrase_w } else { 0 }; let pool = Tui::fill_y(Tui::at_e(PhraseListView(&self.phrases))); let with_pool = move|x|Tui::split_w(false, pool_w, pool, x); - let with_status = |x|Tui::split_n(false, 2, SequencerStatusBar::from(self), x); + let status = SequencerStatusBar::from(self); + let with_status = |x|Tui::split_n(false, if self.status { 2 } else { 0 }, status, x); let with_editbar = |x|Tui::split_n(false, 3, PhraseEditStatus(&self.editor), x); let with_size = |x|lay!([self.size, x]); let editor = with_editbar(with_pool(Tui::fill_xy(&self.editor))); From 93413ae303ab12a92b85e6e289163ee3237e704e Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 17 Dec 2024 18:03:57 +0100 Subject: [PATCH 007/905] stub sampler --- crates/tek/src/tui/app_sampler.rs | 91 ++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 32 deletions(-) diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index bef748fc..cfc9e445 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -51,40 +51,67 @@ pub enum SamplerFocus { } audio!(|self: SamplerTui, _client, _scope|Control::Continue); -render!(|self: SamplerTui|render(|to|{ - let [x, y, w, h] = to.area(); - let style = Some(Style::default().fg(Color::Green)); - to.blit(&"🭚", x, y, style); - to.blit(&"🭥", x + w.saturating_sub(1), y, style); - to.blit(&"🬿", x, y + h.saturating_sub(1), style); - to.blit(&"🭊", x + w.saturating_sub(1), y + h.saturating_sub(1), style); +render!(|self: SamplerTui|{ + Tui::fill_xy(lay!([ - let style = Style::default().gray(); - let voice = self.state.voices.read().unwrap().len(); - let title = format!(" {} ({voice})", self.state.name); - to.blit(&title, x+1, y, Some(style.white().bold().not_dim())); + Tui::fill_xy(render(|to|{ // border + let [x, y, w, h] = to.area(); + let green = Some(Style::default().fg(Color::Green)); + to.blit(&"🭚", x, y, green); + to.blit(&"🭥", x + w.saturating_sub(1), y, green); + to.blit(&"🬿", x, y + h.saturating_sub(1), green); + to.blit(&"🭊", x + w.saturating_sub(1), y + h.saturating_sub(1), green); + Ok(()) + })), - let mut width = title.len() + 2; - let mut y1 = 1; - let mut j = 0; - for (note, sample) in self.state.mapped.iter() - .map(|(note, sample)|(Some(note), sample)) - .chain(self.state.unmapped.iter().map(|sample|(None, sample))) - { - if y1 >= h { - break - } - let active = j == self.cursor.0; - width = width.max( - draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)? - ); - y1 = y1 + 1; - j = j + 1; - } - let h = ((2 + y1) as u16).min(h); - //Ok(Some([x, y, (width as u16).min(to.area().w()), h])) - Ok(()) -})); + col!([ + "", + row!([ + " ", Tui::bold(true, "Sampler."), " Voices: ", + &format!("{}", self.state.voices.read().unwrap().len()), + ]), + " ", + Tui::either(self.state.unmapped.len() > 0, + col!((index, sample) in self.state.unmapped.iter().enumerate() => + { &format!("| Unmapped #{index}") }), " · No unmapped samples."), + " ", + Tui::either(self.state.mapped.len() > 0, + col!((index, sample) in self.state.unmapped.iter().enumerate() => + { &format!("| Mapped #{index}") }), " · No mapped samples."), + ]) + + //render(|to|{ + //let [x, y, w, h] = to.area(); + //let style = Style::default().gray(); + //let voice = self.state.voices.read().unwrap().len(); + //let title = format!(" {} ({voice} voice(s) playing now)", self.state.name); + //to.blit(&title, x+1, y, Some(style.white().bold().not_dim())); + + //let mut width = title.len() + 2; + //let mut y1 = 1; + //let mut j = 0; + //for (note, sample) in self.state.mapped.iter() + //.map(|(note, sample)|(Some(note), sample)) + //.chain(self.state.unmapped.iter().map(|sample|(None, sample))) + //{ + //if y1 >= h { + //break + //} + //let active = j == self.cursor.0; + //width = width.max( + //draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)? + //); + //y1 = y1 + 1; + //j = j + 1; + //} + //let h = ((2 + y1) as u16).min(h); + ////Ok(Some([x, y, (width as u16).min(to.area().w()), h])) + //Ok(()) + //}), + //]) + + ])) +}); impl SamplerTui { /// Immutable reference to sample at cursor. From fdafd15a01bb8689d88204c7a3c6c9c4d77c71d5 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 17 Dec 2024 18:21:30 +0100 Subject: [PATCH 008/905] add command! and input_to_command! macros --- crates/tek/src/core/command.rs | 22 +++- crates/tek/src/tui/app_groovebox.rs | 9 +- crates/tek/src/tui/app_sampler.rs | 117 ++++++++------------ crates/tek/src/tui/app_sequencer.rs | 160 +++++++++++++--------------- 4 files changed, 146 insertions(+), 162 deletions(-) diff --git a/crates/tek/src/core/command.rs b/crates/tek/src/core/command.rs index 480a42e8..92afc4d1 100644 --- a/crates/tek/src/core/command.rs +++ b/crates/tek/src/core/command.rs @@ -1,5 +1,25 @@ use crate::*; +#[macro_export] macro_rules! command { + (|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => { + impl Command<$State> for $Command { + fn execute ($self, $state: &mut $State) -> Perhaps { + Ok($handler) + } + } + } +} + +#[macro_export] macro_rules! input_to_command { + ($Command:ty: <$Engine:ty>|$state:ident:$State:ty,$input:ident|$handler:expr) => { + impl InputToCommand<$Engine, $State> for $Command { + fn input_to_command ($state: &$State, $input: &<$Engine as Engine>::Input) -> Option { + $handler + } + } + } +} + #[derive(Clone)] pub enum NextPrev { Next, @@ -65,7 +85,7 @@ impl> Menu { self } pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self { - self.items.push(MenuItem::cmd(hotkey, text, command)); + self.items.push(MenuItem::cmd(hotkey, text, command)); self } pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self { diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index 7b44bfc5..21dd5376 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -20,6 +20,13 @@ pub struct GrooveboxTui { pub split: u16, } +pub enum GrooveboxCommand { + Sequencer(SequencerCommand), + Sampler(SamplerCommand), +} + render!(|self:GrooveboxTui|Bsp::s(Tui::fixed_y(self.split, &self.sequencer), &self.sampler)); audio!(|self:GrooveboxTui,_client,_process|Control::Continue); -handle!(|self:GrooveboxTui,input|Ok(None)); +handle!(|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input)); +command!(|self:GrooveboxCommand,state:GrooveboxTui|todo!()); +input_to_command!(GrooveboxCommand: |state:GrooveboxTui,input|todo!()); diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index cfc9e445..d4ace6a1 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -8,6 +8,9 @@ use symphonia::core::probe::Hint; use symphonia::core::audio::SampleBuffer; use symphonia::default::get_codecs; +pub enum SamplerCommand { +} + impl TryFrom<&Arc>> for SamplerTui { type Error = Box; fn try_from (jack: &Arc>) -> Usually { @@ -65,54 +68,55 @@ render!(|self: SamplerTui|{ })), col!([ - "", - row!([ - " ", Tui::bold(true, "Sampler."), " Voices: ", + Tui::push_x(2, row!([ + Tui::bold(true, "Sampler"), "|Voices: ", &format!("{}", self.state.voices.read().unwrap().len()), - ]), - " ", + ])), Tui::either(self.state.unmapped.len() > 0, col!((index, sample) in self.state.unmapped.iter().enumerate() => - { &format!("| Unmapped #{index}") }), " · No unmapped samples."), - " ", + { &format!("| Unmapped #{index}") }), "· No unmapped samples."), Tui::either(self.state.mapped.len() > 0, col!((index, sample) in self.state.unmapped.iter().enumerate() => - { &format!("| Mapped #{index}") }), " · No mapped samples."), + { &format!("| Mapped #{index}") }), "· No mapped samples."), ]) - - //render(|to|{ - //let [x, y, w, h] = to.area(); - //let style = Style::default().gray(); - //let voice = self.state.voices.read().unwrap().len(); - //let title = format!(" {} ({voice} voice(s) playing now)", self.state.name); - //to.blit(&title, x+1, y, Some(style.white().bold().not_dim())); - - //let mut width = title.len() + 2; - //let mut y1 = 1; - //let mut j = 0; - //for (note, sample) in self.state.mapped.iter() - //.map(|(note, sample)|(Some(note), sample)) - //.chain(self.state.unmapped.iter().map(|sample|(None, sample))) - //{ - //if y1 >= h { - //break - //} - //let active = j == self.cursor.0; - //width = width.max( - //draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)? - //); - //y1 = y1 + 1; - //j = j + 1; - //} - //let h = ((2 + y1) as u16).min(h); - ////Ok(Some([x, y, (width as u16).min(to.area().w()), h])) - //Ok(()) - //}), - //]) - ])) }); +handle!(|self:SamplerTui,from|{ + let cursor = &mut self.cursor; + let unmapped = &mut self.state.unmapped; + let mapped = &self.state.mapped; + let voices = &self.state.voices; + match from.event() { + key_pat!(KeyCode::Up) => cursor.0 = if cursor.0 == 0 { + mapped.len() + unmapped.len() - 1 + } else { + cursor.0 - 1 + }, + key_pat!(KeyCode::Down) => { + cursor.0 = (cursor.0 + 1) % (mapped.len() + unmapped.len()); + }, + key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { + voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); + }, + key_pat!(KeyCode::Char('a')) => { + let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + unmapped.push(sample); + }, + key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { + *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + }, + key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { + self.editing = Some(sample.clone()); + }, + _ => { + return Ok(None) + } + } + Ok(Some(true)) +}); + impl SamplerTui { /// Immutable reference to sample at cursor. pub fn sample (&self) -> Option<&Arc>> { @@ -256,41 +260,6 @@ fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { todo!(); } -handle!(|self:SamplerTui,from|{ - let cursor = &mut self.cursor; - let unmapped = &mut self.state.unmapped; - let mapped = &self.state.mapped; - let voices = &self.state.voices; - match from.event() { - key_pat!(KeyCode::Up) => cursor.0 = if cursor.0 == 0 { - mapped.len() + unmapped.len() - 1 - } else { - cursor.0 - 1 - }, - key_pat!(KeyCode::Down) => { - cursor.0 = (cursor.0 + 1) % (mapped.len() + unmapped.len()); - }, - key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { - voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); - }, - key_pat!(KeyCode::Char('a')) => { - let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); - unmapped.push(sample); - }, - key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); - }, - key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { - self.editing = Some(sample.clone()); - }, - _ => { - return Ok(None) - } - } - Ok(Some(true)) -}); - fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { let (mut subdirs, mut files) = std::fs::read_dir(dir)? .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 029d79fa..631f56c5 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -54,100 +54,88 @@ pub enum SequencerCommand { ShowPool(bool), } -impl Command for SequencerCommand { - fn execute (self, state: &mut SequencerTui) -> Perhaps { - Ok(match self { - Self::Phrases(cmd) => { - let mut default = |cmd: PhrasesCommand|cmd.execute(&mut state.phrases).map(|x|x.map(Phrases)); - match cmd { - // autoselect: automatically load selected phrase in editor - PhrasesCommand::Select(_) => { - let undo = default(cmd)?; - state.editor.set_phrase(Some(state.phrases.phrase())); - undo - }, - // update color in all places simultaneously - PhrasesCommand::Phrase(SetColor(index, _)) => { - let undo = default(cmd)?; - state.editor.set_phrase(Some(state.phrases.phrase())); - undo - }, - _ => default(cmd)? - } - }, - Self::Editor(cmd) => { - let default = ||cmd.execute(&mut state.editor).map(|x|x.map(Editor)); - match cmd { - _ => default()? - } - }, - Self::Clock(cmd) => cmd.execute(state)?.map(Clock), - Self::Enqueue(phrase) => { - state.player.enqueue_next(phrase.as_ref()); - None - }, - Self::ShowPool(value) => { - state.show_pool = value; - None - } - }) +handle!(|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input)); +input_to_command!(SequencerCommand: |state:SequencerTui,input|Some(match input.event() { + // Transport: Play/pause + key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() { + Play(None) } else { Pause(None) }), + // Transport: Play from start or rewind to start + key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() { + Play(Some(0)) } else { Pause(Some(0)) }), + // TODO: u: undo + key_pat!(Char('u')) => { todo!("undo") }, + // TODO: Shift-U: redo + key_pat!(Char('U')) => { todo!("redo") }, + // TODO: k: toggle on-screen keyboard + key_pat!(Ctrl-Char('k')) => { todo!("keyboard") }, + // Tab: Toggle visibility of phrase pool column + key_pat!(Tab) => ShowPool(!state.show_pool), + // q: Enqueue currently edited phrase + key_pat!(Char('q')) => Enqueue(Some(state.phrases.phrase().clone())), + // 0: Enqueue phrase 0 (stop all) + key_pat!(Char('0')) => Enqueue(Some(state.phrases.phrases()[0].clone())), + // e: Toggle between editing currently playing or other phrase + key_pat!(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() { + let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone()); + let selected = state.phrases.phrase().clone(); + Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing { + selected + } else { + playing.clone() + }))) + } else { + return None + }, + // For the rest, use the default keybindings of the components. + // The ones defined above supersede them. + _ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { + Editor(command) + } else if let Some(command) = PhrasesCommand::input_to_command(&state.phrases, input) { + Phrases(command) + } else { + return None } -} - -impl InputToCommand for SequencerCommand { - fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option { - Some(match input.event() { - // TODO: u: undo - key_pat!(Char('u')) => { todo!("undo") }, - // TODO: Shift-U: redo - key_pat!(Char('U')) => { todo!("redo") }, - // TODO: k: toggle on-screen keyboard - key_pat!(Ctrl-Char('k')) => { todo!("keyboard") }, - // Tab: Toggle visibility of phrase pool column - key_pat!(Tab) => ShowPool(!state.show_pool), - // q: Enqueue currently edited phrase - key_pat!(Char('q')) => Enqueue(Some(state.phrases.phrase().clone())), - // 0: Enqueue phrase 0 (stop all) - key_pat!(Char('0')) => Enqueue(Some(state.phrases.phrases()[0].clone())), - // e: Toggle between editing currently playing or other phrase - key_pat!(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() { - let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone()); - let selected = state.phrases.phrase().clone(); - Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing { - selected - } else { - playing.clone() - }))) - } else { - return None +})); +command!(|self: SequencerCommand, state: SequencerTui|match self { + Self::Phrases(cmd) => { + let mut default = |cmd: PhrasesCommand|cmd.execute(&mut state.phrases).map(|x|x.map(Phrases)); + match cmd { + // autoselect: automatically load selected phrase in editor + PhrasesCommand::Select(_) => { + let undo = default(cmd)?; + state.editor.set_phrase(Some(state.phrases.phrase())); + undo }, - - // Transport: Play/pause - key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() { - Play(None) } else { Pause(None) }), - - // Transport: Play from start or rewind to start - key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() { - Play(Some(0)) } else { Pause(Some(0)) }), - - // For the rest, use the default keybindings of the components. - // The ones defined above supersede them. - _ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { - Editor(command) - } else if let Some(command) = PhrasesCommand::input_to_command(&state.phrases, input) { - Phrases(command) - } else { - return None - } - }) + // update color in all places simultaneously + PhrasesCommand::Phrase(SetColor(index, _)) => { + let undo = default(cmd)?; + state.editor.set_phrase(Some(state.phrases.phrase())); + undo + }, + _ => default(cmd)? + } + }, + Self::Editor(cmd) => { + let default = ||cmd.execute(&mut state.editor).map(|x|x.map(Editor)); + match cmd { + _ => default()? + } + }, + Self::Clock(cmd) => cmd.execute(state)?.map(Clock), + Self::Enqueue(phrase) => { + state.player.enqueue_next(phrase.as_ref()); + None + }, + Self::ShowPool(value) => { + state.show_pool = value; + None } -} +}); has_size!(|self:SequencerTui|&self.size); has_clock!(|self:SequencerTui|&self.clock); has_phrases!(|self:SequencerTui|self.phrases.phrases); has_editor!(|self:SequencerTui|self.editor); -handle!(|self:SequencerTui,i|SequencerCommand::execute_with_state(self, i)); render!(|self: SequencerTui|{ let w = self.size.w(); let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; From 17efdb9b8ee41209c5e819ae226cdb6ed5a083b1 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 17 Dec 2024 18:35:01 +0100 Subject: [PATCH 009/905] use those macros in some places, a few more to go --- crates/tek/src/tui/app_transport.rs | 97 ++++++++------- crates/tek/src/tui/file_browser.rs | 176 ++++++++++++++-------------- crates/tek/src/tui/phrase_list.rs | 112 +++++++++--------- 3 files changed, 192 insertions(+), 193 deletions(-) diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index bc15dddf..baa2ca67 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -218,23 +218,16 @@ pub enum TransportCommand { Clock(ClockCommand), } -impl Command for TransportCommand { - fn execute (self, state: &mut TransportTui) -> Perhaps { - Ok(match self { - Self::Focus(cmd) => cmd.execute(state)?.map(Self::Focus), - Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock), - }) - } -} +command!(|self:TransportCommand,state:TransportTui|match self { + //Self::Focus(cmd) => cmd.execute(state)?.map(Self::Focus), + Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock), + _ => unreachable!(), +}); -impl Command for FocusCommand { - fn execute (self, state: &mut TransportTui) -> Perhaps> { - if let FocusCommand::Set(to) = self { - state.set_focused(to); - } - Ok(None) - } -} +//command!(|self:TransportFocus,state:TransportTui|{ + //if let FocusCommand::Set(to) = self { state.set_focused(to); } + //Ok(None) +//}); impl InputToCommand for TransportCommand { fn input_to_command (state: &TransportTui, input: &TuiInput) -> Option { @@ -262,34 +255,10 @@ where Pause(Some(0)) }), _ => match state.transport_focused().unwrap() { - TransportFocus::Bpm => match input.event() { - key_pat!(Char(',')) => Clock(SetBpm(state.clock().bpm().get() - 1.0)), - key_pat!(Char('.')) => Clock(SetBpm(state.clock().bpm().get() + 1.0)), - key_pat!(Char('<')) => Clock(SetBpm(state.clock().bpm().get() - 0.001)), - key_pat!(Char('>')) => Clock(SetBpm(state.clock().bpm().get() + 0.001)), - _ => return None, - }, - TransportFocus::Quant => match input.event() { - key_pat!(Char(',')) => Clock(SetQuant(state.clock().quant.prev())), - key_pat!(Char('.')) => Clock(SetQuant(state.clock().quant.next())), - key_pat!(Char('<')) => Clock(SetQuant(state.clock().quant.prev())), - key_pat!(Char('>')) => Clock(SetQuant(state.clock().quant.next())), - _ => return None, - }, - TransportFocus::Sync => match input.event() { - key_pat!(Char(',')) => Clock(SetSync(state.clock().sync.prev())), - key_pat!(Char('.')) => Clock(SetSync(state.clock().sync.next())), - key_pat!(Char('<')) => Clock(SetSync(state.clock().sync.prev())), - key_pat!(Char('>')) => Clock(SetSync(state.clock().sync.next())), - _ => return None, - }, - TransportFocus::Clock => match input.event() { - key_pat!(Char(',')) => todo!("transport seek bar"), - key_pat!(Char('.')) => todo!("transport seek bar"), - key_pat!(Char('<')) => todo!("transport seek beat"), - key_pat!(Char('>')) => todo!("transport seek beat"), - _ => return None, - }, + TransportFocus::Bpm => to_bpm_command(input, state.clock().bpm().get())?, + TransportFocus::Quant => to_quant_command(input, &state.clock().quant)?, + TransportFocus::Sync => to_sync_command(input, &state.clock().sync)?, + TransportFocus::Clock => to_seek_command(input)?, TransportFocus::PlayPause => match input.event() { key_pat!(Enter) => Clock( if state.clock().is_stopped() { @@ -310,3 +279,43 @@ where } }) } + +fn to_bpm_command (input: &TuiInput, bpm: f64) -> Option { + Some(match input.event() { + key_pat!(Char(',')) => Clock(SetBpm(bpm - 1.0)), + key_pat!(Char('.')) => Clock(SetBpm(bpm + 1.0)), + key_pat!(Char('<')) => Clock(SetBpm(bpm - 0.001)), + key_pat!(Char('>')) => Clock(SetBpm(bpm + 0.001)), + _ => return None, + }) +} + +fn to_quant_command (input: &TuiInput, quant: &Quantize) -> Option { + Some(match input.event() { + key_pat!(Char(',')) => Clock(SetQuant(quant.prev())), + key_pat!(Char('.')) => Clock(SetQuant(quant.next())), + key_pat!(Char('<')) => Clock(SetQuant(quant.prev())), + key_pat!(Char('>')) => Clock(SetQuant(quant.next())), + _ => return None, + }) +} + +fn to_sync_command (input: &TuiInput, sync: &LaunchSync) -> Option { + Some(match input.event() { + key_pat!(Char(',')) => Clock(SetSync(sync.prev())), + key_pat!(Char('.')) => Clock(SetSync(sync.next())), + key_pat!(Char('<')) => Clock(SetSync(sync.prev())), + key_pat!(Char('>')) => Clock(SetSync(sync.next())), + _ => return None, + }) +} + +fn to_seek_command (input: &TuiInput) -> Option { + Some(match input.event() { + key_pat!(Char(',')) => todo!("transport seek bar"), + key_pat!(Char('.')) => todo!("transport seek bar"), + key_pat!(Char('<')) => todo!("transport seek beat"), + key_pat!(Char('>')) => todo!("transport seek beat"), + _ => return None, + }) +} diff --git a/crates/tek/src/tui/file_browser.rs b/crates/tek/src/tui/file_browser.rs index 5baee32c..cb42feec 100644 --- a/crates/tek/src/tui/file_browser.rs +++ b/crates/tek/src/tui/file_browser.rs @@ -95,100 +95,94 @@ pub enum FileBrowserCommand { Filter(String), } -impl Command for FileBrowserCommand { - fn execute (self, state: &mut PhraseListModel) -> Perhaps { - let mode = state.phrases_mode_mut(); - match mode { - Some(Import(index, ref mut browser)) => match self { - Cancel => { - *mode = None; - }, - Chdir(cwd) => { - *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); - }, - Select(index) => { - browser.index = index; - }, - Confirm => { - if browser.is_file() { - let index = *index; - let path = browser.path(); - *mode = None; - PhrasePoolCommand::Import(index, path).execute(state)?; - } else if browser.is_dir() { - *mode = Some(Import(*index, browser.chdir()?)); - } - }, - _ => todo!(), +command!(|self: FileBrowserCommand, state: PhraseListModel|{ + let mode = state.phrases_mode_mut(); + match mode { + Some(Import(index, ref mut browser)) => match self { + Cancel => { + *mode = None; }, - Some(PhraseListMode::Export(index, ref mut browser)) => match self { - Cancel => { - *mode = None; - }, - Chdir(cwd) => { - *mode = Some(PhraseListMode::Export(*index, FileBrowser::new(Some(cwd))?)); - }, - Select(index) => { - browser.index = index; - }, - _ => unreachable!() + Chdir(cwd) => { + *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); }, - _ => unreachable!(), - }; - Ok(None) - } -} + Select(index) => { + browser.index = index; + }, + Confirm => { + if browser.is_file() { + let index = *index; + let path = browser.path(); + *mode = None; + PhrasePoolCommand::Import(index, path).execute(state)?; + } else if browser.is_dir() { + *mode = Some(Import(*index, browser.chdir()?)); + } + }, + _ => todo!(), + }, + Some(PhraseListMode::Export(index, ref mut browser)) => match self { + Cancel => { + *mode = None; + }, + Chdir(cwd) => { + *mode = Some(PhraseListMode::Export(*index, FileBrowser::new(Some(cwd))?)); + }, + Select(index) => { + browser.index = index; + }, + _ => unreachable!() + }, + _ => unreachable!(), + }; + None +}); -impl InputToCommand for FileBrowserCommand { - fn input_to_command (state: &PhraseListModel, from: &TuiInput) -> Option { - if let Some(PhraseListMode::Import(_index, browser)) = state.phrases_mode() { - Some(match from.event() { - key_pat!(Up) => Select( - browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1)) - ), - key_pat!(Down) => Select( - browser.index.saturating_add(1) % browser.len() - ), - key_pat!(Right) => Chdir(browser.cwd.clone()), - key_pat!(Left) => Chdir(browser.cwd.clone()), - key_pat!(Enter) => Confirm, - key_pat!(Char(_)) => { todo!() }, - key_pat!(Backspace) => { todo!() }, - key_pat!(Esc) => Self::Cancel, - _ => return None - }) - } else if let Some(PhraseListMode::Export(_index, browser)) = state.phrases_mode() { - Some(match from.event() { - key_pat!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())), - key_pat!(Down) => Select(browser.index.saturating_add(1) % browser.len()), - key_pat!(Right) => Chdir(browser.cwd.clone()), - key_pat!(Left) => Chdir(browser.cwd.clone()), - key_pat!(Enter) => Confirm, - key_pat!(Char(_)) => { todo!() }, - key_pat!(Backspace) => { todo!() }, - key_pat!(Esc) => Self::Cancel, - _ => return None - }) - } else { - unreachable!() - } +input_to_command!(FileBrowserCommand:|state:PhraseListModel,from|{ + if let Some(PhraseListMode::Import(_index, browser)) = state.phrases_mode() { + Some(match from.event() { + key_pat!(Up) => Select( + browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1)) + ), + key_pat!(Down) => Select( + browser.index.saturating_add(1) % browser.len() + ), + key_pat!(Right) => Chdir(browser.cwd.clone()), + key_pat!(Left) => Chdir(browser.cwd.clone()), + key_pat!(Enter) => Confirm, + key_pat!(Char(_)) => { todo!() }, + key_pat!(Backspace) => { todo!() }, + key_pat!(Esc) => Self::Cancel, + _ => return None + }) + } else if let Some(PhraseListMode::Export(_index, browser)) = state.phrases_mode() { + Some(match from.event() { + key_pat!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())), + key_pat!(Down) => Select(browser.index.saturating_add(1) % browser.len()), + key_pat!(Right) => Chdir(browser.cwd.clone()), + key_pat!(Left) => Chdir(browser.cwd.clone()), + key_pat!(Enter) => Confirm, + key_pat!(Char(_)) => { todo!() }, + key_pat!(Backspace) => { todo!() }, + key_pat!(Esc) => Self::Cancel, + _ => return None + }) + } else { + unreachable!() } -} +}); -impl InputToCommand for PhraseLengthCommand { - fn input_to_command (state: &PhraseListModel, from: &TuiInput) -> Option { - if let Some(PhraseListMode::Length(_, length, _)) = state.phrases_mode() { - Some(match from.event() { - key_pat!(Up) => Self::Inc, - key_pat!(Down) => Self::Dec, - key_pat!(Right) => Self::Next, - key_pat!(Left) => Self::Prev, - key_pat!(Enter) => Self::Set(*length), - key_pat!(Esc) => Self::Cancel, - _ => return None - }) - } else { - unreachable!() - } +input_to_command!(PhraseLengthCommand:|state:PhraseListModel,from|{ + if let Some(PhraseListMode::Length(_, length, _)) = state.phrases_mode() { + Some(match from.event() { + key_pat!(Up) => Self::Inc, + key_pat!(Down) => Self::Dec, + key_pat!(Right) => Self::Next, + key_pat!(Left) => Self::Prev, + key_pat!(Enter) => Self::Set(*length), + key_pat!(Esc) => Self::Cancel, + _ => return None + }) + } else { + unreachable!() } -} +}); diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index dd0f60ca..8ee899fd 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -50,68 +50,64 @@ pub enum PhrasesCommand { Export(Browse), } -impl Command for PhrasesCommand { - fn execute (self, state: &mut PhraseListModel) -> Perhaps { - use PhrasesCommand::*; - Ok(match self { - Phrase(command) => command.execute(state)?.map(Phrase), - Rename(command) => match command { - PhraseRenameCommand::Begin => { - let length = state.phrases()[state.phrase_index()].read().unwrap().length; - *state.phrases_mode_mut() = Some( - PhraseListMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar) - ); - None - }, - _ => command.execute(state)?.map(Rename) - }, - Length(command) => match command { - PhraseLengthCommand::Begin => { - let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone(); - *state.phrases_mode_mut() = Some( - PhraseListMode::Rename(state.phrase_index(), name) - ); - None - }, - _ => command.execute(state)?.map(Length) - }, - Import(command) => match command { - FileBrowserCommand::Begin => { - *state.phrases_mode_mut() = Some( - PhraseListMode::Import(state.phrase_index(), FileBrowser::new(None)?) - ); - None - }, - _ => command.execute(state)?.map(Import) - }, - Export(command) => match command { - FileBrowserCommand::Begin => { - *state.phrases_mode_mut() = Some( - PhraseListMode::Export(state.phrase_index(), FileBrowser::new(None)?) - ); - None - }, - _ => command.execute(state)?.map(Export) - }, - Select(phrase) => { - state.set_phrase_index(phrase); +command!(|self:PhrasesCommand, state:PhraseListModel|{ + use PhrasesCommand::*; + match self { + Phrase(command) => command.execute(state)?.map(Phrase), + Rename(command) => match command { + PhraseRenameCommand::Begin => { + let length = state.phrases()[state.phrase_index()].read().unwrap().length; + *state.phrases_mode_mut() = Some( + PhraseListMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar) + ); None }, - }) + _ => command.execute(state)?.map(Rename) + }, + Length(command) => match command { + PhraseLengthCommand::Begin => { + let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone(); + *state.phrases_mode_mut() = Some( + PhraseListMode::Rename(state.phrase_index(), name) + ); + None + }, + _ => command.execute(state)?.map(Length) + }, + Import(command) => match command { + FileBrowserCommand::Begin => { + *state.phrases_mode_mut() = Some( + PhraseListMode::Import(state.phrase_index(), FileBrowser::new(None)?) + ); + None + }, + _ => command.execute(state)?.map(Import) + }, + Export(command) => match command { + FileBrowserCommand::Begin => { + *state.phrases_mode_mut() = Some( + PhraseListMode::Export(state.phrase_index(), FileBrowser::new(None)?) + ); + None + }, + _ => command.execute(state)?.map(Export) + }, + Select(phrase) => { + state.set_phrase_index(phrase); + None + }, } -} +}); -impl InputToCommand for PhrasesCommand { - fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option { - Some(match state.phrases_mode() { - Some(PhraseListMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?), - Some(PhraseListMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?), - Some(PhraseListMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?), - Some(PhraseListMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?), - _ => to_phrases_command(state, input)? - }) - } -} +input_to_command!(PhrasesCommand:|state:PhraseListModel,input|{ + Some(match state.phrases_mode() { + Some(PhraseListMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?), + Some(PhraseListMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?), + Some(PhraseListMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?), + Some(PhraseListMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?), + _ => to_phrases_command(state, input)? + }) +}); fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option { use KeyCode::{Up, Down, Delete, Char}; From 9bd898ab337278496c2efa8363990cc40d46c64a Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 17 Dec 2024 18:51:27 +0100 Subject: [PATCH 010/905] stub sampler import command --- crates/tek/src/core/command.rs | 2 +- crates/tek/src/tui/app_groovebox.rs | 7 ++++++- crates/tek/src/tui/app_sampler.rs | 1 + crates/tek/src/tui/app_sequencer.rs | 4 ++-- crates/tek/src/tui/file_browser.rs | 12 ++++++------ crates/tek/src/tui/phrase_list.rs | 14 ++++++-------- 6 files changed, 22 insertions(+), 18 deletions(-) diff --git a/crates/tek/src/core/command.rs b/crates/tek/src/core/command.rs index 92afc4d1..1f59e548 100644 --- a/crates/tek/src/core/command.rs +++ b/crates/tek/src/core/command.rs @@ -14,7 +14,7 @@ use crate::*; ($Command:ty: <$Engine:ty>|$state:ident:$State:ty,$input:ident|$handler:expr) => { impl InputToCommand<$Engine, $State> for $Command { fn input_to_command ($state: &$State, $input: &<$Engine as Engine>::Input) -> Option { - $handler + Some($handler) } } } diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index 21dd5376..808eab63 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -1,5 +1,6 @@ use crate::*; use super::*; +use KeyCode::Char; impl TryFrom<&Arc>> for GrooveboxTui { type Error = Box; @@ -28,5 +29,9 @@ pub enum GrooveboxCommand { render!(|self:GrooveboxTui|Bsp::s(Tui::fixed_y(self.split, &self.sequencer), &self.sampler)); audio!(|self:GrooveboxTui,_client,_process|Control::Continue); handle!(|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input)); +input_to_command!(GrooveboxCommand: |state:GrooveboxTui,input|match input.event() { + // load sample + key_pat!(Char('l')) => GrooveboxCommand::Sampler(SamplerCommand::Import), + _ => GrooveboxCommand::Sequencer(SequencerCommand::input_to_command(&state.sequencer, input)?), +}); command!(|self:GrooveboxCommand,state:GrooveboxTui|todo!()); -input_to_command!(GrooveboxCommand: |state:GrooveboxTui,input|todo!()); diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index d4ace6a1..e65b7bf1 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -9,6 +9,7 @@ use symphonia::core::audio::SampleBuffer; use symphonia::default::get_codecs; pub enum SamplerCommand { + Import } impl TryFrom<&Arc>> for SamplerTui { diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 631f56c5..95aa078d 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -55,7 +55,7 @@ pub enum SequencerCommand { } handle!(|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input)); -input_to_command!(SequencerCommand: |state:SequencerTui,input|Some(match input.event() { +input_to_command!(SequencerCommand: |state:SequencerTui,input|match input.event() { // Transport: Play/pause key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), @@ -95,7 +95,7 @@ input_to_command!(SequencerCommand: |state:SequencerTui,input|Some(match in } else { return None } -})); +}); command!(|self: SequencerCommand, state: SequencerTui|match self { Self::Phrases(cmd) => { let mut default = |cmd: PhrasesCommand|cmd.execute(&mut state.phrases).map(|x|x.map(Phrases)); diff --git a/crates/tek/src/tui/file_browser.rs b/crates/tek/src/tui/file_browser.rs index cb42feec..7b33bf0a 100644 --- a/crates/tek/src/tui/file_browser.rs +++ b/crates/tek/src/tui/file_browser.rs @@ -139,7 +139,7 @@ command!(|self: FileBrowserCommand, state: PhraseListModel|{ input_to_command!(FileBrowserCommand:|state:PhraseListModel,from|{ if let Some(PhraseListMode::Import(_index, browser)) = state.phrases_mode() { - Some(match from.event() { + match from.event() { key_pat!(Up) => Select( browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1)) ), @@ -153,9 +153,9 @@ input_to_command!(FileBrowserCommand:|state:PhraseListModel,from|{ key_pat!(Backspace) => { todo!() }, key_pat!(Esc) => Self::Cancel, _ => return None - }) + } } else if let Some(PhraseListMode::Export(_index, browser)) = state.phrases_mode() { - Some(match from.event() { + match from.event() { key_pat!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())), key_pat!(Down) => Select(browser.index.saturating_add(1) % browser.len()), key_pat!(Right) => Chdir(browser.cwd.clone()), @@ -165,7 +165,7 @@ input_to_command!(FileBrowserCommand:|state:PhraseListModel,from|{ key_pat!(Backspace) => { todo!() }, key_pat!(Esc) => Self::Cancel, _ => return None - }) + } } else { unreachable!() } @@ -173,7 +173,7 @@ input_to_command!(FileBrowserCommand:|state:PhraseListModel,from|{ input_to_command!(PhraseLengthCommand:|state:PhraseListModel,from|{ if let Some(PhraseListMode::Length(_, length, _)) = state.phrases_mode() { - Some(match from.event() { + match from.event() { key_pat!(Up) => Self::Inc, key_pat!(Down) => Self::Dec, key_pat!(Right) => Self::Next, @@ -181,7 +181,7 @@ input_to_command!(PhraseLengthCommand:|state:PhraseListModel,from|{ key_pat!(Enter) => Self::Set(*length), key_pat!(Esc) => Self::Cancel, _ => return None - }) + } } else { unreachable!() } diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 8ee899fd..c515e202 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -99,14 +99,12 @@ command!(|self:PhrasesCommand, state:PhraseListModel|{ } }); -input_to_command!(PhrasesCommand:|state:PhraseListModel,input|{ - Some(match state.phrases_mode() { - Some(PhraseListMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?), - Some(PhraseListMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?), - Some(PhraseListMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?), - Some(PhraseListMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?), - _ => to_phrases_command(state, input)? - }) +input_to_command!(PhrasesCommand:|state:PhraseListModel,input|match state.phrases_mode() { + Some(PhraseListMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?), + Some(PhraseListMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?), + Some(PhraseListMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?), + Some(PhraseListMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?), + _ => to_phrases_command(state, input)? }); fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option { From da39c84ba4609b7731a408dfb0658c3803eb317b Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 17 Dec 2024 19:26:16 +0100 Subject: [PATCH 011/905] Tui::fixed_ -> Fixed::w/h/wh --- crates/tek/src/layout/fixed.rs | 28 +++++++++++--------------- crates/tek/src/tui/app_arranger.rs | 10 ++++----- crates/tek/src/tui/app_groovebox.rs | 12 +++++++++-- crates/tek/src/tui/app_sampler.rs | 10 +++++++++ crates/tek/src/tui/app_sequencer.rs | 6 +++--- crates/tek/src/tui/app_transport.rs | 2 +- crates/tek/src/tui/phrase_editor.rs | 8 ++++---- crates/tek/src/tui/phrase_list.rs | 2 +- crates/tek/src/tui/piano_horizontal.rs | 6 +++--- 9 files changed, 49 insertions(+), 35 deletions(-) diff --git a/crates/tek/src/layout/fixed.rs b/crates/tek/src/layout/fixed.rs index a73c54ba..78d3d41d 100644 --- a/crates/tek/src/layout/fixed.rs +++ b/crates/tek/src/layout/fixed.rs @@ -1,21 +1,7 @@ use crate::*; -impl LayoutFixed for E {} - -pub trait LayoutFixed { - fn fixed_x > (x: E::Unit, w: W) -> Fixed { - Fixed::X(x, w) - } - fn fixed_y > (y: E::Unit, w: W) -> Fixed { - Fixed::Y(y, w) - } - fn fixed_xy > (x: E::Unit, y: E::Unit, w: W) -> Fixed { - Fixed::XY(x, y, w) - } -} - /// Enforce fixed size of drawing area -pub enum Fixed { +pub enum Fixed> { _Unused(PhantomData), /// Enforce fixed width X(E::Unit, T), @@ -25,7 +11,7 @@ pub enum Fixed { XY(E::Unit, E::Unit, T), } -impl Fixed { +impl> Fixed { pub fn inner (&self) -> &T { match self { Self::X(_, i) => i, @@ -34,7 +20,17 @@ impl Fixed { _ => unreachable!(), } } + pub fn w (x: E::Unit, w: T) -> Fixed { + Self::X(x, w) + } + pub fn h (y: E::Unit, w: T) -> Fixed { + Self::Y(y, w) + } + pub fn wh (x: E::Unit, y: E::Unit, w: T) -> Fixed { + Self::XY(x, y, w) + } } + impl> Render for Fixed { fn min_size (&self, to: E::Size) -> Perhaps { Ok(match self { diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 858647b4..9a50ad7f 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -80,7 +80,7 @@ render!(|self: ArrangerTui|{ add(&self.size) }))); with_transport(col!([ - Tui::fixed_y(self.splits[0], lay!([ + Fixed::h(self.splits[0], lay!([ arranger(), Tui::push_x(1, Tui::fg( TuiTheme::title_fg(arranger_focused), @@ -877,16 +877,16 @@ impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVerticalContent<'a> { } } } -render!(|self: ArrangerVerticalContent<'a>|Tui::fixed_y( +render!(|self: ArrangerVerticalContent<'a>|Fixed::h( (self.size.h() as u16).saturating_sub(self.header_h), col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => { let height = 1.max((pulses / PPQ) as u16); let playing = scene.is_playing(self.tracks); - Tui::fixed_y(height, row!([ + Fixed::h(height, row!([ if playing { "▶ " } else { " " }, Tui::bold(true, scene.name.read().unwrap().as_str()), row!((track, w) in self.cols.iter().map(|col|col.0).enumerate() => { - Tui::fixed_xy(w as u16, height, Layers::new(move |add|{ + Fixed::wh(w as u16, height, Layers::new(move |add|{ let mut bg = TuiTheme::border_bg(); match (self.tracks.get(track), scene.clips.get(track)) { (Some(track), Some(Some(phrase))) => { @@ -900,7 +900,7 @@ render!(|self: ArrangerVerticalContent<'a>|Tui::fixed_y( bg = color.light.rgb } }; - add(&Tui::fixed_x(w as u16, Tui::push_x(1, &name.as_str()[0..max_w])))?; + add(&Fixed::w(w as u16, Tui::push_x(1, &name.as_str()[0..max_w])))?; }, _ => {} }; diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index 808eab63..727da6fd 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -26,7 +26,10 @@ pub enum GrooveboxCommand { Sampler(SamplerCommand), } -render!(|self:GrooveboxTui|Bsp::s(Tui::fixed_y(self.split, &self.sequencer), &self.sampler)); +render!(|self:GrooveboxTui|Bsp::n( + Fixed::h(2, SequencerStatusBar::from(&self.sequencer)), + Bsp::s(Fixed::h(self.split, &self.sequencer), &self.sampler), +)); audio!(|self:GrooveboxTui,_client,_process|Control::Continue); handle!(|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input)); input_to_command!(GrooveboxCommand: |state:GrooveboxTui,input|match input.event() { @@ -34,4 +37,9 @@ input_to_command!(GrooveboxCommand: |state:GrooveboxTui,input|match input.e key_pat!(Char('l')) => GrooveboxCommand::Sampler(SamplerCommand::Import), _ => GrooveboxCommand::Sequencer(SequencerCommand::input_to_command(&state.sequencer, input)?), }); -command!(|self:GrooveboxCommand,state:GrooveboxTui|todo!()); +command!(|self:GrooveboxCommand,state:GrooveboxTui|match self { + GrooveboxCommand::Sequencer(command) => + command.execute(&mut state.sequencer)?.map(GrooveboxCommand::Sequencer), + GrooveboxCommand::Sampler(command) => + command.execute(&mut state.sampler)?.map(GrooveboxCommand::Sampler), +}); diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index e65b7bf1..f744c510 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -11,6 +11,16 @@ use symphonia::default::get_codecs; pub enum SamplerCommand { Import } +input_to_command!(SamplerCommand: |state:SamplerTui,input|match input.event() { + _ => return None +}); +command!(|self:SamplerCommand,state:SamplerTui|match self { + SamplerCommand::Import => { + None + }, + _ => todo!() +}); + impl TryFrom<&Arc>> for SamplerTui { type Error = Box; diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 95aa078d..cd16778a 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -148,8 +148,8 @@ render!(|self: SequencerTui|{ let with_size = |x|lay!([self.size, x]); let editor = with_editbar(with_pool(Tui::fill_xy(&self.editor))); let color = self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten().clone(); - let play = Tui::fixed_xy(5, 2, PlayPause(self.clock.is_rolling())); - let transport = Tui::fixed_y(2, TransportView::from((self, color, true))); + let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); + let transport = Fixed::h(2, TransportView::from((self, color, true))); let toolbar = row!([play, col!([ PhraseSelector::play_phrase(&self.player), PhraseSelector::next_phrase(&self.player), @@ -225,7 +225,7 @@ impl From<&SequencerTui> for SequencerStatusBar { } } -render!(|self: SequencerStatusBar|Tui::fixed_y(2, lay!([ +render!(|self: SequencerStatusBar|Fixed::h(2, lay!([ { let single = |binding, command|row!([" ", col!([ Tui::fg(TuiTheme::yellow(), binding), diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index baa2ca67..745f05b8 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -126,7 +126,7 @@ render!(|self: TransportView|{ pub struct PlayPause(pub bool); render!(|self: PlayPause|Tui::bg( if self.0{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}, - Tui::fixed_x(5, col!(|add|if self.0 { + Fixed::w(5, col!(|add|if self.0 { add(&Tui::fg(Color::Rgb(0, 255, 0), col!([ " 🭍🭑🬽 ", " 🭞🭜🭘 ", diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index eede64c2..c5273c0b 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -245,12 +245,12 @@ render!(|self:PhraseEditStatus<'a>|row!(|add|{ &y ]); add(&Tui::fill_x(Tui::fg_bg(fg, bg, row!(|add|{ - add(&Tui::fixed_xy(26, 3, col!(![ + add(&Fixed::wh(26, 3, col!(![ field(" Edit", format!("{name}")), field(" Length", format!("{length}")), field(" Loop", format!("{looped}")), ])))?; - add(&Tui::fixed_xy(25, 3, col!(![ + add(&Fixed::wh(25, 3, col!(![ field(" Time", format!("{}", self.0.time_point())), field(" View", format!("{}-{} ({}*{})", @@ -259,7 +259,7 @@ render!(|self:PhraseEditStatus<'a>|row!(|add|{ self.0.time_axis(), self.0.time_zoom())) ])))?; - add(&Tui::fixed_xy(25, 3, col!(![ + add(&Fixed::wh(25, 3, col!(![ field(" Note", format!("{:4} ({:3}) {:4}", to_note_name(self.0.note_point()), self.0.note_point(), @@ -269,7 +269,7 @@ render!(|self:PhraseEditStatus<'a>|row!(|add|{ to_note_name(self.0.note_hi()), self.0.note_axis())) ])))?; - add(&Tui::fixed_xy(16, 3, col!(![ + add(&Fixed::wh(16, 3, col!(![ row!(!["TimeLock ", Tui::bold(true, format!("{}", self.0.time_lock()))])])))?; Ok(()) })))) diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index c515e202..32c063a5 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -263,7 +263,7 @@ pub struct PhraseSelector { } // TODO: Display phrases always in order of appearance -render!(|self: PhraseSelector|Tui::fixed_xy(24, 1, row!([ +render!(|self: PhraseSelector|Fixed::wh(24, 1, row!([ Tui::fg(self.color.lightest.rgb, Tui::bold(true, &self.title)), Tui::fg_bg(self.color.lighter.rgb, self.color.base.rgb, row!([ format!("{:8}", &self.name[0..8.min(self.name.len())]), diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index a28f9877..46bfc7e4 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -68,12 +68,12 @@ render!(|self: PianoHorizontal|{ let keys_width = 5; Tui::fill_xy(Tui::bg(color.darker.rgb, Bsp::s( - Tui::fixed_y(1, Bsp::e( - Tui::fixed_x(keys_width, ""), + Fixed::h(1, Bsp::e( + Fixed::w(keys_width, ""), Tui::fill_x(timeline()), )), Bsp::e( - Tui::fixed_x(keys_width, keys()), + Fixed::w(keys_width, keys()), Tui::fill_xy(lay!([&self.size, Tui::fill_xy(lay!([ Tui::fill_xy(notes()), Tui::fill_xy(cursor()), From e127924227c8a9743e63d3f9ec77e7e039f15bd3 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 17 Dec 2024 19:32:10 +0100 Subject: [PATCH 012/905] Tui::fill_ -> Fill::w/h/wh --- crates/tek/src/layout/fill.rs | 23 +++++++++-------------- crates/tek/src/layout/fixed.rs | 6 +++--- crates/tek/src/tui/app_arranger.rs | 4 ++-- crates/tek/src/tui/app_sampler.rs | 4 ++-- crates/tek/src/tui/app_sequencer.rs | 6 +++--- crates/tek/src/tui/app_transport.rs | 2 +- crates/tek/src/tui/phrase_editor.rs | 2 +- crates/tek/src/tui/phrase_list.rs | 14 +++++++------- crates/tek/src/tui/piano_horizontal.rs | 10 +++++----- crates/tek/src/tui/tui_border.rs | 2 +- 10 files changed, 34 insertions(+), 39 deletions(-) diff --git a/crates/tek/src/layout/fill.rs b/crates/tek/src/layout/fill.rs index 28ba7066..044d751a 100644 --- a/crates/tek/src/layout/fill.rs +++ b/crates/tek/src/layout/fill.rs @@ -1,19 +1,5 @@ use crate::*; -impl LayoutFill for E {} - -pub trait LayoutFill { - fn fill_x > (fill: W) -> Fill { - Fill::X(fill) - } - fn fill_y > (fill: W) -> Fill { - Fill::Y(fill) - } - fn fill_xy > (fill: W) -> Fill { - Fill::XY(fill) - } -} - pub enum Fill> { X(W), Y(W), @@ -30,6 +16,15 @@ impl> Fill { _ => unreachable!(), } } + pub fn w (fill: W) -> Self { + Self::X(fill) + } + pub fn h (fill: W) -> Self { + Self::Y(fill) + } + pub fn wh (fill: W) -> Self { + Self::XY(fill) + } } impl> Render for Fill { diff --git a/crates/tek/src/layout/fixed.rs b/crates/tek/src/layout/fixed.rs index 78d3d41d..63db372b 100644 --- a/crates/tek/src/layout/fixed.rs +++ b/crates/tek/src/layout/fixed.rs @@ -20,13 +20,13 @@ impl> Fixed { _ => unreachable!(), } } - pub fn w (x: E::Unit, w: T) -> Fixed { + pub fn w (x: E::Unit, w: T) -> Self { Self::X(x, w) } - pub fn h (y: E::Unit, w: T) -> Fixed { + pub fn h (y: E::Unit, w: T) -> Self { Self::Y(y, w) } - pub fn wh (x: E::Unit, y: E::Unit, w: T) -> Fixed { + pub fn wh (x: E::Unit, y: E::Unit, w: T) -> Self { Self::XY(x, y, w) } } diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 9a50ad7f..9fe0c3ff 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -588,7 +588,7 @@ render!(|self: ArrangerStatus|{ }; //let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}")); - Tui::bg(status_bar_bg, Tui::fill_x(row!([mode, commands]))) + Tui::bg(status_bar_bg, Fill::w(row!([mode, commands]))) }); @@ -645,7 +645,7 @@ pub fn arranger_content_vertical ( factor: usize ) -> impl Render + use<'_> { lay!([ - Tui::at_se(Tui::fill_xy(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(view.arranger_focused()), + Tui::at_se(Fill::wh(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(view.arranger_focused()), format!("{}x{}", view.size.w(), view.size.h())) ))), Tui::bg(view.color.rgb, lay!(![ diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index f744c510..7855f70d 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -66,9 +66,9 @@ pub enum SamplerFocus { audio!(|self: SamplerTui, _client, _scope|Control::Continue); render!(|self: SamplerTui|{ - Tui::fill_xy(lay!([ + Fill::wh(lay!([ - Tui::fill_xy(render(|to|{ // border + Fill::wh(render(|to|{ // border let [x, y, w, h] = to.area(); let green = Some(Style::default().fg(Color::Green)); to.blit(&"🭚", x, y, green); diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index cd16778a..e719c8d1 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -140,13 +140,13 @@ render!(|self: SequencerTui|{ let w = self.size.w(); let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; let pool_w = if self.show_pool { phrase_w } else { 0 }; - let pool = Tui::fill_y(Tui::at_e(PhraseListView(&self.phrases))); + let pool = Fill::h(Tui::at_e(PhraseListView(&self.phrases))); let with_pool = move|x|Tui::split_w(false, pool_w, pool, x); let status = SequencerStatusBar::from(self); let with_status = |x|Tui::split_n(false, if self.status { 2 } else { 0 }, status, x); let with_editbar = |x|Tui::split_n(false, 3, PhraseEditStatus(&self.editor), x); let with_size = |x|lay!([self.size, x]); - let editor = with_editbar(with_pool(Tui::fill_xy(&self.editor))); + let editor = with_editbar(with_pool(Fill::wh(&self.editor))); let color = self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten().clone(); let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); let transport = Fixed::h(2, TransportView::from((self, color, true))); @@ -244,7 +244,7 @@ render!(|self: SequencerStatusBar|Fixed::h(2, lay!([ double(("q", "enqueue"), ("e", "edit"), ), ])) }, - Tui::fill_xy(Tui::at_se({ + Fill::wh(Tui::at_se({ Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), row!([ &self.cpu, &self.res, diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index 745f05b8..aa3a67c4 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -108,7 +108,7 @@ render!(|self: TransportView|{ Tui::fg_bg(self.2.lighter.rgb, self.2.base.rgb, format!("{:>10}", self.1)), ])); - Tui::bg(color.base.rgb, Tui::fill_x(row!([ + Tui::bg(color.base.rgb, Fill::w(row!([ //PlayPause(self.started), " ", col!([ Field(" Beat", self.beat.as_str(), &color), diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index c5273c0b..f3b5fab4 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -244,7 +244,7 @@ render!(|self:PhraseEditStatus<'a>|row!(|add|{ Tui::fg_bg(color.lighter.rgb, Color::Reset, Tui::bold(true, "│")), &y ]); - add(&Tui::fill_x(Tui::fg_bg(fg, bg, row!(|add|{ + add(&Fill::w(Tui::fg_bg(fg, bg, row!(|add|{ add(&Fixed::wh(26, 3, col!(![ field(" Edit", format!("{name}")), field(" Length", format!("{length}")), diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 32c063a5..fe23cbb1 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -214,7 +214,7 @@ render!(|self: PhraseListView<'a>|{ let upper_right = format!("({})", phrases.len()); Tui::bg(bg, lay!(move|add|{ //add(&Lozenge(Style::default().bg(border_bg).fg(border_color)))?; - add(&Tui::inset_xy(0, 1, Tui::fill_xy(col!(move|add|match mode { + add(&Tui::inset_xy(0, 1, Fill::wh(col!(move|add|match mode { Some(PhraseListMode::Import(_, ref file_picker)) => add(file_picker), Some(PhraseListMode::Export(_, ref file_picker)) => add(file_picker), _ => Ok(for (i, phrase) in phrases.iter().enumerate() { @@ -227,10 +227,10 @@ render!(|self: PhraseListView<'a>|{ length.focus = Some(*focus); } } - add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([ - Tui::fill_x(lay!(|add|{ - add(&Tui::fill_x(Tui::at_w(format!(" {i}"))))?; - add(&Tui::fill_x(Tui::at_e(Tui::pull_x(1, length.clone())))) + add(&Tui::bg(color.base.rgb, Fill::w(col!([ + Fill::w(lay!(|add|{ + add(&Fill::w(Tui::at_w(format!(" {i}"))))?; + add(&Fill::w(Tui::at_e(Tui::pull_x(1, length.clone())))) })), Tui::bold(true, { let mut row2 = format!(" {name}"); @@ -249,8 +249,8 @@ render!(|self: PhraseListView<'a>|{ }))?; }) }))))?; - add(&Tui::fill_x(Tui::at_nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))))?; - add(&Tui::fill_x(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))))?; + add(&Fill::w(Tui::at_nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))))?; + add(&Fill::w(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))))?; add(&self.0.size) })) }); diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index 46bfc7e4..eb348464 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -66,17 +66,17 @@ render!(|self: PianoHorizontal|{ let notes = move||PianoHorizontalNotes(&self); let cursor = move||PianoHorizontalCursor(&self); let keys_width = 5; - Tui::fill_xy(Tui::bg(color.darker.rgb, + Fill::wh(Tui::bg(color.darker.rgb, Bsp::s( Fixed::h(1, Bsp::e( Fixed::w(keys_width, ""), - Tui::fill_x(timeline()), + Fill::w(timeline()), )), Bsp::e( Fixed::w(keys_width, keys()), - Tui::fill_xy(lay!([&self.size, Tui::fill_xy(lay!([ - Tui::fill_xy(notes()), - Tui::fill_xy(cursor()), + Fill::wh(lay!([&self.size, Fill::wh(lay!([ + Fill::wh(notes()), + Fill::wh(cursor()), ]))])), ), ) diff --git a/crates/tek/src/tui/tui_border.rs b/crates/tek/src/tui/tui_border.rs index 76e615d9..e7a38a3e 100644 --- a/crates/tek/src/tui/tui_border.rs +++ b/crates/tek/src/tui/tui_border.rs @@ -3,7 +3,7 @@ use crate::*; pub struct Bordered>(pub S, pub W); render!(|self: Bordered>|{ - Tui::fill_xy(lay!([Border(self.0), Tui::inset_xy(1, 1, widget(&self.1))])) + Fill::wh(lay!([Border(self.0), Tui::inset_xy(1, 1, widget(&self.1))])) }); pub struct Border(pub S); From 914b5698397dcdd6b1ec08ab2fa7f65c4c669f57 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 17 Dec 2024 19:36:43 +0100 Subject: [PATCH 013/905] Tui::at_ -> Align::_ --- crates/tek/src/layout/align.rs | 60 ++++++++++++++--------------- crates/tek/src/tui/app_arranger.rs | 2 +- crates/tek/src/tui/app_sequencer.rs | 4 +- crates/tek/src/tui/phrase_list.rs | 8 ++-- 4 files changed, 36 insertions(+), 38 deletions(-) diff --git a/crates/tek/src/layout/align.rs b/crates/tek/src/layout/align.rs index 2e11791a..9cfa1ec5 100644 --- a/crates/tek/src/layout/align.rs +++ b/crates/tek/src/layout/align.rs @@ -1,48 +1,33 @@ use crate::*; -impl LayoutAlign for E {} - -pub trait LayoutAlign { - fn center_x > (w: W) -> Align { Align::X(w) } - fn center_y > (w: W) -> Align { Align::Y(w) } - fn center > (w: W) -> Align { Align::Center(w) } - fn at_n > (w: W) -> Align { Align::N(w) } - fn at_s > (w: W) -> Align { Align::S(w) } - fn at_e > (w: W) -> Align { Align::E(w) } - fn at_w > (w: W) -> Align { Align::W(w) } - fn at_nw > (w: W) -> Align { Align::NW(w) } - fn at_sw > (w: W) -> Align { Align::SW(w) } - fn at_ne > (w: W) -> Align { Align::NE(w) } - fn at_se > (w: W) -> Align { Align::SE(w) } -} - /// Override X and Y coordinates, aligning to corner, side, or center of area -pub enum Align { +pub enum Align> { + _Unused(PhantomData), /// Draw at center of container - Center(L), + Center(T), /// Draw at center of X axis - X(L), + X(T), /// Draw at center of Y axis - Y(L), + Y(T), /// Draw at upper left corner of contaier - NW(L), + NW(T), /// Draw at center of upper edge of container - N(L), + N(T), /// Draw at right left corner of contaier - NE(L), + NE(T), /// Draw at center of left edge of container - W(L), + W(T), /// Draw at center of right edge of container - E(L), + E(T), /// Draw at lower left corner of container - SW(L), + SW(T), /// Draw at center of lower edge of container - S(L), + S(T), /// Draw at lower right edge of container - SE(L) + SE(T) } -impl Align { +impl> Align { pub fn inner (&self) -> &T { match self { Self::Center(inner) => inner, @@ -56,11 +41,23 @@ impl Align { Self::SW(inner) => inner, Self::S(inner) => inner, Self::SE(inner) => inner, + _ => unreachable!(), } } + pub fn c (w: T) -> Self { Self::Center(w) } + pub fn x (w: T) -> Self { Self::X(w) } + pub fn y (w: T) -> Self { Self::Y(w) } + pub fn n (w: T) -> Self { Self::N(w) } + pub fn s (w: T) -> Self { Self::S(w) } + pub fn e (w: T) -> Self { Self::E(w) } + pub fn w (w: T) -> Self { Self::W(w) } + pub fn nw (w: T) -> Self { Self::NW(w) } + pub fn sw (w: T) -> Self { Self::SW(w) } + pub fn ne (w: T) -> Self { Self::NE(w) } + pub fn se (w: T) -> Self { Self::SE(w) } } -fn align + From<[N;4]>> (align: &Align, outer: R, inner: R) -> Option { +fn align, N: Coordinate, R: Area + From<[N;4]>> (align: &Align, outer: R, inner: R) -> Option { if outer.w() < inner.w() || outer.h() < inner.h() { None } else { @@ -78,11 +75,12 @@ fn align + From<[N;4]>> (align: &Align, outer: R Align::SW(_) => [ox, oy + oh - ih, iw, ih,].into(), Align::S(_) => [ox + (ow - iw) / 2.into(), oy + oh - ih, iw, ih,].into(), Align::SE(_) => [ox + ow - iw, oy + oh - ih, iw, ih,].into(), + _ => unreachable!() }) } } -impl> Render for Align { +impl> Render for Align { fn min_size (&self, outer_area: E::Size) -> Perhaps { self.inner().min_size(outer_area) } diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 9fe0c3ff..a0fc36f1 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -645,7 +645,7 @@ pub fn arranger_content_vertical ( factor: usize ) -> impl Render + use<'_> { lay!([ - Tui::at_se(Fill::wh(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(view.arranger_focused()), + Align::se(Fill::wh(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(view.arranger_focused()), format!("{}x{}", view.size.w(), view.size.h())) ))), Tui::bg(view.color.rgb, lay!(![ diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index e719c8d1..8386fd12 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -140,7 +140,7 @@ render!(|self: SequencerTui|{ let w = self.size.w(); let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; let pool_w = if self.show_pool { phrase_w } else { 0 }; - let pool = Fill::h(Tui::at_e(PhraseListView(&self.phrases))); + let pool = Fill::h(Align::e(PhraseListView(&self.phrases))); let with_pool = move|x|Tui::split_w(false, pool_w, pool, x); let status = SequencerStatusBar::from(self); let with_status = |x|Tui::split_n(false, if self.status { 2 } else { 0 }, status, x); @@ -244,7 +244,7 @@ render!(|self: SequencerStatusBar|Fixed::h(2, lay!([ double(("q", "enqueue"), ("e", "edit"), ), ])) }, - Fill::wh(Tui::at_se({ + Fill::wh(Align::se({ Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), row!([ &self.cpu, &self.res, diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index fe23cbb1..71d57d3d 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -229,8 +229,8 @@ render!(|self: PhraseListView<'a>|{ } add(&Tui::bg(color.base.rgb, Fill::w(col!([ Fill::w(lay!(|add|{ - add(&Fill::w(Tui::at_w(format!(" {i}"))))?; - add(&Fill::w(Tui::at_e(Tui::pull_x(1, length.clone())))) + add(&Fill::w(Align::w(format!(" {i}"))))?; + add(&Fill::w(Align::e(Tui::pull_x(1, length.clone())))) })), Tui::bold(true, { let mut row2 = format!(" {name}"); @@ -249,8 +249,8 @@ render!(|self: PhraseListView<'a>|{ }))?; }) }))))?; - add(&Fill::w(Tui::at_nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))))?; - add(&Fill::w(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))))?; + add(&Fill::w(Align::nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))))?; + add(&Fill::w(Align::ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))))?; add(&self.0.size) })) }); From 3e4d75ea408d99c2a1ae4d34bd683619e3f47341 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 17 Dec 2024 20:02:47 +0100 Subject: [PATCH 014/905] parameterize render! macro --- crates/tek/examples/bsp.rs | 120 +++++++++++++++++++++++++ crates/tek/src/core/output.rs | 13 +++ crates/tek/src/layout/bsp.rs | 10 +-- crates/tek/src/layout/measure.rs | 2 +- crates/tek/src/tui.rs | 13 --- crates/tek/src/tui/app_arranger.rs | 14 +-- crates/tek/src/tui/app_groovebox.rs | 2 +- crates/tek/src/tui/app_sampler.rs | 2 +- crates/tek/src/tui/app_sequencer.rs | 4 +- crates/tek/src/tui/app_transport.rs | 10 +-- crates/tek/src/tui/file_browser.rs | 2 +- crates/tek/src/tui/phrase_editor.rs | 6 +- crates/tek/src/tui/phrase_length.rs | 3 +- crates/tek/src/tui/phrase_list.rs | 4 +- crates/tek/src/tui/piano_horizontal.rs | 32 +++---- crates/tek/src/tui/port_select.rs | 2 +- crates/tek/src/tui/tui_border.rs | 2 +- 17 files changed, 176 insertions(+), 65 deletions(-) create mode 100644 crates/tek/examples/bsp.rs diff --git a/crates/tek/examples/bsp.rs b/crates/tek/examples/bsp.rs new file mode 100644 index 00000000..b1894d70 --- /dev/null +++ b/crates/tek/examples/bsp.rs @@ -0,0 +1,120 @@ +use tek_core::*; +use tek_core::jack::*; + +fn main () -> Usually<()> { + Tui::run(Arc::new(RwLock::new(Demo::new())))?; + Ok(()) +} + +pub struct BspDemo; + +impl Content for Demo { + type Engine = Tui; + fn content (&self) -> dyn Render { + let border_style = Style::default().fg(Color::Rgb(0,0,0)); + Align::Center(Layers::new(move|add|{ + + add(&Background(Color::Rgb(0,128,128)))?; + + add(&Outset::XY(1, 1, Stack::down(|add|{ + + add(&Layers::new(|add|{ + add(&Background(Color::Rgb(128,96,0)))?; + add(&Border(Square(border_style)))?; + add(&Outset::XY(2, 1, "..."))?; + Ok(()) + }).debug())?; + + add(&Layers::new(|add|{ + add(&Background(Color::Rgb(128,64,0)))?; + add(&Border(Lozenge(border_style)))?; + add(&Outset::XY(4, 2, "---"))?; + Ok(()) + }).debug())?; + + add(&Layers::new(|add|{ + add(&Background(Color::Rgb(96,64,0)))?; + add(&Border(SquareBold(border_style)))?; + add(&Outset::XY(6, 3, "~~~"))?; + Ok(()) + }).debug())?; + + Ok(()) + })).debug())?; + + Ok(()) + + })) + //Align::Center(Outset::X(1, Layers::new(|add|{ + //add(&Background(Color::Rgb(128,0,0)))?; + //add(&Stack::down(|add|{ + //add(&Outset::Y(1, Layers::new(|add|{ + //add(&Background(Color::Rgb(0,128,0)))?; + //add(&Align::Center("12345"))?; + //add(&Align::Center("FOO")) + //})))?; + //add(&Outset::XY(1, 1, Layers::new(|add|{ + //add(&Align::Center("1234567"))?; + //add(&Align::Center("BAR"))?; + //add(&Background(Color::Rgb(0,0,128))) + //}))) + //})) + //}))) + + //Align::Y(Layers::new(|add|{ + //add(&Background(Color::Rgb(128,0,0)))?; + //add(&Outset::X(1, Align::Center(Stack::down(|add|{ + //add(&Align::X(Outset::Y(1, Layers::new(|add|{ + //add(&Background(Color::Rgb(0,128,0)))?; + //add(&Align::Center("12345"))?; + //add(&Align::Center("FOO")) + //})))?; + //add(&Outset::XY(1, 1, Layers::new(|add|{ + //add(&Align::Center("1234567"))?; + //add(&Align::Center("BAR"))?; + //add(&Background(Color::Rgb(0,0,128))) + //})))?; + //Ok(()) + //}))))) + //})) + } +} + +impl Handle for Demo { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + match from.event() { + key!(KeyCode::PageUp) => { + self.index = (self.index + 1) % self.items.len(); + }, + key!(KeyCode::PageDown) => { + self.index = if self.index > 1 { + self.index - 1 + } else { + self.items.len() - 1 + }; + }, + _ => return Ok(None) + } + Ok(Some(true)) + } +} + +//lisp!(CONTENT Demo (LET + //(BORDER-STYLE (STYLE (FG (RGB 0 0 0)))) + //(BG-COLOR-0 (RGB 0 128 128)) + //(BG-COLOR-1 (RGB 128 96 0)) + //(BG-COLOR-2 (RGB 128 64 0)) + //(BG-COLOR-3 (RGB 96 64 0)) + //(CENTER (LAYERS + //(BACKGROUND BG-COLOR-0) + //(OUTSET-XY 1 1 (SPLIT-DOWN + //(LAYERS (BACKGROUND BG-COLOR-1) + //(BORDER SQUARE BORDER-STYLE) + //(OUTSET-XY 2 1 "...")) + //(LAYERS (BACKGROUND BG-COLOR-2) + //(BORDER LOZENGE BORDER-STYLE) + //(OUTSET-XY 4 2 "---")) + //(LAYERS (BACKGROUND BG-COLOR-3) + //(BORDER SQUARE-BOLD BORDER-STYLE) + //(OUTSET-XY 2 1 "~~~")))))))) + diff --git a/crates/tek/src/core/output.rs b/crates/tek/src/core/output.rs index 9b2ea0ff..55c8ce8a 100644 --- a/crates/tek/src/core/output.rs +++ b/crates/tek/src/core/output.rs @@ -1,5 +1,18 @@ use crate::*; +#[macro_export] macro_rules! render { + (<$E:ty>|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? Render<$E> for $Struct $(<$($L),*$($T),*>)? { + fn min_size (&$self, to: <$E as Engine>::Size) -> Perhaps<<$E as Engine>::Size> { + $cb.min_size(to) + } + fn render (&$self, to: &mut <$E as Engine>::Output) -> Usually<()> { + $cb.render(to) + } + } + } +} + /// Rendering target pub trait Output { /// Current output area diff --git a/crates/tek/src/layout/bsp.rs b/crates/tek/src/layout/bsp.rs index 083761e1..181f7fbd 100644 --- a/crates/tek/src/layout/bsp.rs +++ b/crates/tek/src/layout/bsp.rs @@ -67,28 +67,28 @@ impl, Y: Render> Render for Bsp { Self::Null(_) => {}, Self::S(a, b) => { let s_a = a.min_size(s)?.unwrap_or(n); - //let s_b = b.min_size(s)?.unwrap_or(n); + let _ = b.min_size(s)?.unwrap_or(n); let h = s_a.h().into(); to.render_in(to.area().clip_h(h).into(), a)?; - to.render_in(to.area().push_y(h).shrink_y(h).into(), b)?; + to.render_in(to.area().shrink_y(h).push_y(h).into(), b)?; }, Self::E(a, b) => { let s_a = a.min_size(s)?.unwrap_or(n); - //let s_b = b.min_size(s)?.unwrap_or(n); + let _ = b.min_size(s)?.unwrap_or(n); let w = s_a.w().into(); to.render_in(to.area().clip_w(w).into(), a)?; to.render_in(to.area().push_x(w).shrink_x(w).into(), b)?; }, Self::W(a, b) => { let s_a = a.min_size(s)?.unwrap_or(n); - //let s_b = b.min_size(s)?.unwrap_or(n); + let _ = b.min_size(s)?.unwrap_or(n); let w = (to.area().w() - s_a.w()).into(); to.render_in(to.area().push_x(w).into(), a)?; to.render_in(to.area().shrink_x(w).into(), b)?; }, Self::N(a, b) => { let s_a = a.min_size(s)?.unwrap_or(n); - //let s_b = b.min_size(s)?.unwrap_or(n); + let _ = b.min_size(s)?.unwrap_or(n); let h = to.area().h() - s_a.h(); to.render_in(to.area().push_y(h).into(), a)?; to.render_in(to.area().shrink_y(h).into(), b)?; diff --git a/crates/tek/src/layout/measure.rs b/crates/tek/src/layout/measure.rs index 987fe8a5..1a1fcd40 100644 --- a/crates/tek/src/layout/measure.rs +++ b/crates/tek/src/layout/measure.rs @@ -84,7 +84,7 @@ impl Measure { } pub struct ShowMeasure<'a>(&'a Measure); -render!(|self: ShowMeasure<'a>|render(|to|Ok({ +render!(|self: ShowMeasure<'a>|render(|to|Ok({ let w = self.0.w(); let h = self.0.h(); to.blit(&format!(" {w} x {h} "), to.area.x(), to.area.y(), Some( diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index 9bdda0f8..75e7c016 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -27,19 +27,6 @@ mod port_select; pub(crate) use port_select::*; //////////////////////////////////////////////////////// -#[macro_export] macro_rules! render { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? Render for $Struct $(<$($L),*$($T),*>)? { - fn min_size (&$self, to: [u16;2]) -> Perhaps<[u16;2]> { - $cb.min_size(to) - } - fn render (&$self, to: &mut TuiOutput) -> Usually<()> { - $cb.render(to) - } - } - } -} - pub fn render Usually<()>+Send+Sync> (render: F) -> impl Render { Widget::new(|_|Ok(Some([0u16,0u16].into())), render) } diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index a0fc36f1..b2c8c6d4 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -60,7 +60,7 @@ has_clock!(|self:ArrangerTui|&self.clock); has_phrases!(|self:ArrangerTui|self.phrases.phrases); has_editor!(|self:ArrangerTui|self.editor); handle!(|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input)); -render!(|self: ArrangerTui|{ +render!(|self: ArrangerTui|{ let arranger_focused = self.arranger_focused(); let transport_focused = if let ArrangerFocus::Transport(_) = self.focus.inner() { true @@ -506,7 +506,7 @@ impl StatusBar for ArrangerStatus { } } -render!(|self: ArrangerStatus|{ +render!(|self: ArrangerStatus|{ let label = match self { Self::Transport => "TRANSPORT", @@ -674,7 +674,7 @@ impl From<&ArrangerTui> for ArrangerVerticalColumnSeparator { } } } -render!(|self: ArrangerVerticalColumnSeparator|render(move|to: &mut TuiOutput|{ +render!(|self: ArrangerVerticalColumnSeparator|render(move|to: &mut TuiOutput|{ let style = Some(Style::default().fg(self.sep_fg)); Ok(for x in self.cols.iter().map(|col|col.1) { let x = self.scenes_w + to.area().x() + x as u16; @@ -697,7 +697,7 @@ impl From<(&ArrangerTui, usize)> for ArrangerVerticalRowSeparator { } } -render!(|self: ArrangerVerticalRowSeparator|render(move|to: &mut TuiOutput|{ +render!(|self: ArrangerVerticalRowSeparator|render(move|to: &mut TuiOutput|{ Ok(for y in self.rows.iter().map(|row|row.1) { let y = to.area().y() + (y / PPQ) as u16 + 1; if y >= to.buffer.area.height { break } @@ -731,7 +731,7 @@ impl From<(&ArrangerTui, usize)> for ArrangerVerticalCursor { } } } -render!(|self: ArrangerVerticalCursor|render(move|to: &mut TuiOutput|{ +render!(|self: ArrangerVerticalCursor|render(move|to: &mut TuiOutput|{ let area = to.area(); let focused = self.focused; let selected = self.selected; @@ -810,7 +810,7 @@ impl<'a> From<&'a ArrangerTui> for ArrangerVerticalHeader<'a> { } } } -render!(|self: ArrangerVerticalHeader<'a>|row!( +render!(|self: ArrangerVerticalHeader<'a>|row!( (track, w) in self.tracks.iter().zip(self.cols.iter().map(|col|col.0)) => { // name and width of track let name = track.name().read().unwrap(); @@ -877,7 +877,7 @@ impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVerticalContent<'a> { } } } -render!(|self: ArrangerVerticalContent<'a>|Fixed::h( +render!(|self: ArrangerVerticalContent<'a>|Fixed::h( (self.size.h() as u16).saturating_sub(self.header_h), col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => { let height = 1.max((pulses / PPQ) as u16); diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index 727da6fd..84542517 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -26,7 +26,7 @@ pub enum GrooveboxCommand { Sampler(SamplerCommand), } -render!(|self:GrooveboxTui|Bsp::n( +render!(|self:GrooveboxTui|Bsp::n( Fixed::h(2, SequencerStatusBar::from(&self.sequencer)), Bsp::s(Fixed::h(self.split, &self.sequencer), &self.sampler), )); diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index 7855f70d..005a023b 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -65,7 +65,7 @@ pub enum SamplerFocus { } audio!(|self: SamplerTui, _client, _scope|Control::Continue); -render!(|self: SamplerTui|{ +render!(|self: SamplerTui|{ Fill::wh(lay!([ Fill::wh(render(|to|{ // border diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 8386fd12..0dcc3f0d 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -136,7 +136,7 @@ has_size!(|self:SequencerTui|&self.size); has_clock!(|self:SequencerTui|&self.clock); has_phrases!(|self:SequencerTui|self.phrases.phrases); has_editor!(|self:SequencerTui|self.editor); -render!(|self: SequencerTui|{ +render!(|self: SequencerTui|{ let w = self.size.w(); let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; let pool_w = if self.show_pool { phrase_w } else { 0 }; @@ -225,7 +225,7 @@ impl From<&SequencerTui> for SequencerStatusBar { } } -render!(|self: SequencerStatusBar|Fixed::h(2, lay!([ +render!(|self: SequencerStatusBar|Fixed::h(2, lay!([ { let single = |binding, command|row!([" ", col!([ Tui::fg(TuiTheme::yellow(), binding), diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index aa3a67c4..419c0656 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -40,7 +40,7 @@ impl std::fmt::Debug for TransportTui { has_clock!(|self:TransportTui|&self.clock); audio!(|self:TransportTui,client,scope|ClockAudio(self).process(client, scope)); handle!(|self:TransportTui,from|TransportCommand::execute_with_state(self, from)); -render!(|self: TransportTui|TransportView::from((self, None, true))); +render!(|self: TransportTui|TransportView::from((self, None, true))); pub struct TransportView { color: ItemPalette, @@ -97,12 +97,12 @@ impl From<(&T, Option, bool)> for TransportView { } -render!(|self: TransportView|{ +render!(|self: TransportView|{ let color = self.color; struct Field<'a>(&'a str, &'a str, &'a ItemPalette); - render!(|self: Field<'a>|row!([ + render!(|self: Field<'a>|row!([ Tui::fg_bg(self.2.lightest.rgb, self.2.darkest.rgb, Tui::bold(true, self.0)), Tui::fg_bg(self.2.lighter.rgb, self.2.darkest.rgb, "│"), Tui::fg_bg(self.2.lighter.rgb, self.2.base.rgb, format!("{:>10}", self.1)), @@ -124,7 +124,7 @@ render!(|self: TransportView|{ }); pub struct PlayPause(pub bool); -render!(|self: PlayPause|Tui::bg( +render!(|self: PlayPause|Tui::bg( if self.0{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}, Fixed::w(5, col!(|add|if self.0 { add(&Tui::fg(Color::Rgb(0, 255, 0), col!([ @@ -200,7 +200,7 @@ impl StatusBar for TransportStatusBar { } } -render!(|self: TransportStatusBar|"todo"); +render!(|self: TransportStatusBar|"todo"); pub trait TransportControl: HasClock + { fn transport_focused (&self) -> Option; diff --git a/crates/tek/src/tui/file_browser.rs b/crates/tek/src/tui/file_browser.rs index 7b33bf0a..9d265a70 100644 --- a/crates/tek/src/tui/file_browser.rs +++ b/crates/tek/src/tui/file_browser.rs @@ -15,7 +15,7 @@ pub struct FileBrowser { pub size: Measure } -render!(|self: FileBrowser|{ +render!(|self: FileBrowser|{ Stack::down(|add|{ let mut i = 0; for (_, name) in self.dirs.iter() { diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index f3b5fab4..44b50a3f 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -119,8 +119,8 @@ impl Default for PhraseEditorModel { } has_size!(|self:PhraseEditorModel|&self.size); -render!(|self: PhraseEditorModel|&self.mode); -//render!(|self: PhraseEditorModel|lay!(|add|{add(&self.size)?;add(self.mode)}));//bollocks +render!(|self: PhraseEditorModel|&self.mode); +//render!(|self: PhraseEditorModel|lay!(|add|{add(&self.size)?;add(self.mode)}));//bollocks pub trait PhraseViewMode: Render + HasSize + MidiRange + MidiPoint + Debug + Send + Sync { fn buffer_size (&self, phrase: &Phrase) -> (usize, usize); @@ -231,7 +231,7 @@ impl std::fmt::Debug for PhraseEditorModel { } pub struct PhraseEditStatus<'a>(pub &'a PhraseEditorModel); -render!(|self:PhraseEditStatus<'a>|row!(|add|{ +render!(|self:PhraseEditStatus<'a>|row!(|add|{ let (color, name, length, looped) = if let Some(phrase) = self.0.phrase().as_ref().map(|p|p.read().unwrap()) { (phrase.color, phrase.name.clone(), phrase.length, phrase.looped) } else { diff --git a/crates/tek/src/tui/phrase_length.rs b/crates/tek/src/tui/phrase_length.rs index f6f49cde..aa862aa1 100644 --- a/crates/tek/src/tui/phrase_length.rs +++ b/crates/tek/src/tui/phrase_length.rs @@ -68,8 +68,7 @@ impl PhraseLengthFocus { } } - -render!(|self: PhraseLength|{ +render!(|self: PhraseLength|{ let bars = ||self.bars_string(); let beats = ||self.beats_string(); let ticks = ||self.ticks_string(); diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 71d57d3d..8130336b 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -206,7 +206,7 @@ pub trait HasPhraseList: HasPhrases { pub struct PhraseListView<'a>(pub(crate) &'a PhraseListModel); // TODO: Display phrases always in order of appearance -render!(|self: PhraseListView<'a>|{ +render!(|self: PhraseListView<'a>|{ let PhraseListModel { phrases, mode, .. } = self.0; let bg = TuiTheme::g(32); let title_color = TuiTheme::ti1(); @@ -263,7 +263,7 @@ pub struct PhraseSelector { } // TODO: Display phrases always in order of appearance -render!(|self: PhraseSelector|Fixed::wh(24, 1, row!([ +render!(|self: PhraseSelector|Fixed::wh(24, 1, row!([ Tui::fg(self.color.lightest.rgb, Tui::bold(true, &self.title)), Tui::fg_bg(self.color.lighter.rgb, self.color.base.rgb, row!([ format!("{:8}", &self.name[0..8.min(self.name.len())]), diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index eb348464..2990ff98 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -59,32 +59,24 @@ impl PianoHorizontal { } } -render!(|self: PianoHorizontal|{ +render!(|self: PianoHorizontal|{ let color = self.color; let keys = move||PianoHorizontalKeys(&self); let timeline = move||PianoHorizontalTimeline(&self); let notes = move||PianoHorizontalNotes(&self); let cursor = move||PianoHorizontalCursor(&self); let keys_width = 5; - Fill::wh(Tui::bg(color.darker.rgb, - Bsp::s( - Fixed::h(1, Bsp::e( - Fixed::w(keys_width, ""), - Fill::w(timeline()), - )), - Bsp::e( - Fixed::w(keys_width, keys()), - Fill::wh(lay!([&self.size, Fill::wh(lay!([ - Fill::wh(notes()), - Fill::wh(cursor()), - ]))])), - ), - ) - )) + Tui::bg(color.darker.rgb, Fill::wh(Bsp::s( + Fixed::h(1, Bsp::e(Fixed::w(keys_width, ""), Fill::w(timeline()),)), + Bsp::e( + Fixed::w(keys_width, keys()), + Fill::wh(lay!([&self.size, Fill::wh(lay!([Fill::wh(notes()), Fill::wh(cursor()),]))])), + ), + ))) }); pub struct PianoHorizontalTimeline<'a>(&'a PianoHorizontal); -render!(|self: PianoHorizontalTimeline<'a>|render(|to|{ +render!(|self: PianoHorizontalTimeline<'a>|render(|to|{ let [x, y, w, h] = to.area(); let style = Some(Style::default().dim()); let length = self.0.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); @@ -103,7 +95,7 @@ render!(|self: PianoHorizontalTimeline<'a>|render(|to|{ //)); pub struct PianoHorizontalKeys<'a>(&'a PianoHorizontal); -render!(|self: PianoHorizontalKeys<'a>|render(|to|Ok({ +render!(|self: PianoHorizontalKeys<'a>|render(|to|Ok({ let color = self.0.color; let note_lo = self.0.note_lo(); let note_hi = self.0.note_hi(); @@ -136,7 +128,7 @@ render!(|self: PianoHorizontalKeys<'a>|render(|to|Ok({ }))); pub struct PianoHorizontalCursor<'a>(&'a PianoHorizontal); -render!(|self: PianoHorizontalCursor<'a>|render(|to|Ok({ +render!(|self: PianoHorizontalCursor<'a>|render(|to|Ok({ let note_hi = self.0.note_hi(); let note_len = self.0.note_len(); let note_lo = self.0.note_lo(); @@ -167,7 +159,7 @@ render!(|self: PianoHorizontalCursor<'a>|render(|to|Ok({ }))); pub struct PianoHorizontalNotes<'a>(&'a PianoHorizontal); -render!(|self: PianoHorizontalNotes<'a>|render(|to|Ok({ +render!(|self: PianoHorizontalNotes<'a>|render(|to|Ok({ let time_start = self.0.time_start(); let note_hi = self.0.note_hi(); let note_lo = self.0.note_lo(); diff --git a/crates/tek/src/tui/port_select.rs b/crates/tek/src/tui/port_select.rs index 3c71dfb0..1850a253 100644 --- a/crates/tek/src/tui/port_select.rs +++ b/crates/tek/src/tui/port_select.rs @@ -4,4 +4,4 @@ pub struct PortSelector { pub(crate) title: &'static str, } -render!(|self: PortSelector|{}); +render!(|self: PortSelector|{}); diff --git a/crates/tek/src/tui/tui_border.rs b/crates/tek/src/tui/tui_border.rs index e7a38a3e..b7860f6d 100644 --- a/crates/tek/src/tui/tui_border.rs +++ b/crates/tek/src/tui/tui_border.rs @@ -2,7 +2,7 @@ use crate::*; pub struct Bordered>(pub S, pub W); -render!(|self: Bordered>|{ +render!(|self: Bordered>|{ Fill::wh(lay!([Border(self.0), Tui::inset_xy(1, 1, widget(&self.1))])) }); From 950dbdfe8e9ce1584e5de7a423d081f52b73433e Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 17 Dec 2024 20:22:57 +0100 Subject: [PATCH 015/905] wip: demo_bsp --- crates/tek/examples/bsp.rs | 120 ----------------------------- crates/tek/examples/demo.rs | 3 +- crates/tek/examples/demo_bsp.rs | 19 +++++ crates/tek/src/core.rs | 30 +++++--- crates/tek/src/core/output.rs | 10 +++ crates/tek/src/layout.rs | 34 +++++---- crates/tek/src/lib.rs | 131 +++++++++++++++++--------------- crates/tek/src/tui.rs | 21 +++-- 8 files changed, 154 insertions(+), 214 deletions(-) delete mode 100644 crates/tek/examples/bsp.rs create mode 100644 crates/tek/examples/demo_bsp.rs diff --git a/crates/tek/examples/bsp.rs b/crates/tek/examples/bsp.rs deleted file mode 100644 index b1894d70..00000000 --- a/crates/tek/examples/bsp.rs +++ /dev/null @@ -1,120 +0,0 @@ -use tek_core::*; -use tek_core::jack::*; - -fn main () -> Usually<()> { - Tui::run(Arc::new(RwLock::new(Demo::new())))?; - Ok(()) -} - -pub struct BspDemo; - -impl Content for Demo { - type Engine = Tui; - fn content (&self) -> dyn Render { - let border_style = Style::default().fg(Color::Rgb(0,0,0)); - Align::Center(Layers::new(move|add|{ - - add(&Background(Color::Rgb(0,128,128)))?; - - add(&Outset::XY(1, 1, Stack::down(|add|{ - - add(&Layers::new(|add|{ - add(&Background(Color::Rgb(128,96,0)))?; - add(&Border(Square(border_style)))?; - add(&Outset::XY(2, 1, "..."))?; - Ok(()) - }).debug())?; - - add(&Layers::new(|add|{ - add(&Background(Color::Rgb(128,64,0)))?; - add(&Border(Lozenge(border_style)))?; - add(&Outset::XY(4, 2, "---"))?; - Ok(()) - }).debug())?; - - add(&Layers::new(|add|{ - add(&Background(Color::Rgb(96,64,0)))?; - add(&Border(SquareBold(border_style)))?; - add(&Outset::XY(6, 3, "~~~"))?; - Ok(()) - }).debug())?; - - Ok(()) - })).debug())?; - - Ok(()) - - })) - //Align::Center(Outset::X(1, Layers::new(|add|{ - //add(&Background(Color::Rgb(128,0,0)))?; - //add(&Stack::down(|add|{ - //add(&Outset::Y(1, Layers::new(|add|{ - //add(&Background(Color::Rgb(0,128,0)))?; - //add(&Align::Center("12345"))?; - //add(&Align::Center("FOO")) - //})))?; - //add(&Outset::XY(1, 1, Layers::new(|add|{ - //add(&Align::Center("1234567"))?; - //add(&Align::Center("BAR"))?; - //add(&Background(Color::Rgb(0,0,128))) - //}))) - //})) - //}))) - - //Align::Y(Layers::new(|add|{ - //add(&Background(Color::Rgb(128,0,0)))?; - //add(&Outset::X(1, Align::Center(Stack::down(|add|{ - //add(&Align::X(Outset::Y(1, Layers::new(|add|{ - //add(&Background(Color::Rgb(0,128,0)))?; - //add(&Align::Center("12345"))?; - //add(&Align::Center("FOO")) - //})))?; - //add(&Outset::XY(1, 1, Layers::new(|add|{ - //add(&Align::Center("1234567"))?; - //add(&Align::Center("BAR"))?; - //add(&Background(Color::Rgb(0,0,128))) - //})))?; - //Ok(()) - //}))))) - //})) - } -} - -impl Handle for Demo { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - match from.event() { - key!(KeyCode::PageUp) => { - self.index = (self.index + 1) % self.items.len(); - }, - key!(KeyCode::PageDown) => { - self.index = if self.index > 1 { - self.index - 1 - } else { - self.items.len() - 1 - }; - }, - _ => return Ok(None) - } - Ok(Some(true)) - } -} - -//lisp!(CONTENT Demo (LET - //(BORDER-STYLE (STYLE (FG (RGB 0 0 0)))) - //(BG-COLOR-0 (RGB 0 128 128)) - //(BG-COLOR-1 (RGB 128 96 0)) - //(BG-COLOR-2 (RGB 128 64 0)) - //(BG-COLOR-3 (RGB 96 64 0)) - //(CENTER (LAYERS - //(BACKGROUND BG-COLOR-0) - //(OUTSET-XY 1 1 (SPLIT-DOWN - //(LAYERS (BACKGROUND BG-COLOR-1) - //(BORDER SQUARE BORDER-STYLE) - //(OUTSET-XY 2 1 "...")) - //(LAYERS (BACKGROUND BG-COLOR-2) - //(BORDER LOZENGE BORDER-STYLE) - //(OUTSET-XY 4 2 "---")) - //(LAYERS (BACKGROUND BG-COLOR-3) - //(BORDER SQUARE-BOLD BORDER-STYLE) - //(OUTSET-XY 2 1 "~~~")))))))) - diff --git a/crates/tek/examples/demo.rs b/crates/tek/examples/demo.rs index ba0e022c..cfe62794 100644 --- a/crates/tek/examples/demo.rs +++ b/crates/tek/examples/demo.rs @@ -1,5 +1,4 @@ -use tek_core::*; -use tek_core::jack::*; +use tek::*; fn main () -> Usually<()> { Tui::run(Arc::new(RwLock::new(Demo::new())))?; diff --git a/crates/tek/examples/demo_bsp.rs b/crates/tek/examples/demo_bsp.rs new file mode 100644 index 00000000..42b8d8e6 --- /dev/null +++ b/crates/tek/examples/demo_bsp.rs @@ -0,0 +1,19 @@ +use tek::{core::*, layout::*, tui::*, *}; +use std::sync::{Arc, RwLock}; + +fn main () -> Usually<()> { + Tui::run(Arc::new(RwLock::new(BspDemo(Default::default()))))?; + Ok(()) +} + +pub struct BspDemo(std::marker::PhantomData); + +render!(|self:BspDemo|Fill::wh(Align::c( + Bsp::n(Bsp::s(Bsp::e(Bsp::w("00", "11"), "22"), "33"), "44") +))); + +impl Handle for BspDemo { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + Ok(None) + } +} diff --git a/crates/tek/src/core.rs b/crates/tek/src/core.rs index 029f46ac..a0e94939 100644 --- a/crates/tek/src/core.rs +++ b/crates/tek/src/core.rs @@ -1,12 +1,18 @@ -mod audio; pub(crate) use audio::*; -mod color; pub(crate) use color::*; -mod command; pub(crate) use command::*; -mod edn; pub(crate) use edn::*; -mod engine; pub(crate) use engine::*; -mod focus; pub(crate) use focus::*; -mod input; pub(crate) use input::*; -mod output; pub(crate) use output::*; -mod perf; pub(crate) use perf::*; -mod pitch; pub(crate) use pitch::*; -mod space; pub(crate) use space::*; -mod time; pub(crate) use time::*; +pub(crate) mod audio; pub(crate) use audio::*; +pub(crate) mod color; pub(crate) use color::*; +pub(crate) mod command; pub(crate) use command::*; +pub(crate) mod edn; pub(crate) use edn::*; +pub(crate) mod engine; pub(crate) use engine::*; +pub(crate) mod focus; pub(crate) use focus::*; +pub(crate) mod input; pub(crate) use input::*; +pub(crate) mod output; pub(crate) use output::*; +pub(crate) mod perf; pub(crate) use perf::*; +pub(crate) mod pitch; pub(crate) use pitch::*; +pub(crate) mod space; pub(crate) use space::*; +pub(crate) mod time; pub(crate) use time::*; + +pub use self::{ + engine::Engine, + input::Handle, + output::Render +}; diff --git a/crates/tek/src/core/output.rs b/crates/tek/src/core/output.rs index 55c8ce8a..8798251f 100644 --- a/crates/tek/src/core/output.rs +++ b/crates/tek/src/core/output.rs @@ -1,6 +1,16 @@ use crate::*; #[macro_export] macro_rules! render { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*E$(,$T:ident$(:$U:path)?)*$(,)?>)?|$cb:expr) => { + impl Render for $Struct $(<$($L,)* E, $($T),*>)? { + fn min_size (&$self, to: ::Size) -> Perhaps<::Size> { + $cb.min_size(to) + } + fn render (&$self, to: &mut ::Output) -> Usually<()> { + $cb.render(to) + } + } + }; (<$E:ty>|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? Render<$E> for $Struct $(<$($L),*$($T),*>)? { fn min_size (&$self, to: <$E as Engine>::Size) -> Perhaps<<$E as Engine>::Size> { diff --git a/crates/tek/src/layout.rs b/crates/tek/src/layout.rs index c3007b8e..89f4db97 100644 --- a/crates/tek/src/layout.rs +++ b/crates/tek/src/layout.rs @@ -1,14 +1,20 @@ -mod align; pub(crate) use align::*; -mod bsp; pub(crate) use bsp::*; -mod cond; pub(crate) use cond::*; -mod fill; pub(crate) use fill::*; -mod fixed; pub(crate) use fixed::*; -mod inset_outset; pub(crate) use inset_outset::*; -mod layers; pub(crate) use layers::*; -mod measure; pub(crate) use measure::*; -mod min_max; pub(crate) use min_max::*; -mod push_pull; pub(crate) use push_pull::*; -mod scroll; pub(crate) use scroll::*; -mod shrink_grow; pub(crate) use shrink_grow::*; -mod split; pub(crate) use split::*; -mod stack; pub(crate) use stack::*; +pub(crate) mod align; pub(crate) use align::*; +pub(crate) mod bsp; pub(crate) use bsp::*; +pub(crate) mod cond; pub(crate) use cond::*; +pub(crate) mod fill; pub(crate) use fill::*; +pub(crate) mod fixed; pub(crate) use fixed::*; +pub(crate) mod inset_outset; pub(crate) use inset_outset::*; +pub(crate) mod layers; pub(crate) use layers::*; +pub(crate) mod measure; pub(crate) use measure::*; +pub(crate) mod min_max; pub(crate) use min_max::*; +pub(crate) mod push_pull; pub(crate) use push_pull::*; +pub(crate) mod scroll; pub(crate) use scroll::*; +pub(crate) mod shrink_grow; pub(crate) use shrink_grow::*; +pub(crate) mod split; pub(crate) use split::*; +pub(crate) mod stack; pub(crate) use stack::*; + +pub use self::{ + align::*, + bsp::*, + fill::*, +}; diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs index 84ad431d..a6378d05 100644 --- a/crates/tek/src/lib.rs +++ b/crates/tek/src/lib.rs @@ -1,57 +1,3 @@ -pub(crate) use std::sync::{Arc, Mutex, RwLock}; -pub(crate) use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize}; -pub(crate) use std::collections::BTreeMap; -pub(crate) use std::marker::PhantomData; -pub(crate) use std::thread::{spawn, JoinHandle}; -pub(crate) use std::path::PathBuf; -pub(crate) use std::ffi::OsString; -pub(crate) use std::time::Duration; -pub(crate) use std::io::{Stdout, stdout}; -pub(crate) use std::error::Error; - -pub(crate) use ratatui; -pub(crate) use ratatui::{ - prelude::{Style, Color, Buffer}, - style::{Stylize, Modifier}, - backend::{Backend, CrosstermBackend, ClearType} -}; - -pub(crate) use jack; -pub(crate) use jack::{ - contrib::ClosureProcessHandler, - Client, ProcessScope, Control, CycleTimes, - Port, PortSpec, MidiIn, MidiOut, AudioIn, AudioOut, Unowned, - Transport, TransportState, MidiIter, RawMidi, -}; - -pub(crate) use midly; -pub(crate) use midly::{ - Smf, - MidiMessage, - TrackEventKind, - live::LiveEvent, - num::u7 -}; - -pub(crate) use palette::{ - *, - convert::*, - okhsl::* -}; - -pub(crate) use clap::{self, Parser}; - -pub(crate) use crossterm::{ExecutableCommand}; -pub(crate) use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}; -pub(crate) use crossterm::event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState}; -pub(crate) use better_panic::{Settings, Verbosity}; - -pub(crate) use atomic_float::*; - -use std::ops::{Add, Sub, Mul, Div, Rem}; -use std::cmp::{Ord, Eq, PartialEq}; -use std::fmt::{Debug, Display}; - /// Standard result type. pub type Usually = Result>; @@ -73,11 +19,74 @@ pub type Perhaps = Result, Box>; ($($name:ident)*) => { $(#[cfg(test)] mod $name;)* }; } -mod core; pub(crate) use core::*; -mod layout; pub(crate) use layout::*; -mod api; pub(crate) use api::*; -mod tui; pub(crate) use tui::*; +pub mod core; +pub(crate) use core::*; -testmod! { - test -} +pub mod layout; pub(crate) +use layout::*; + +pub mod api; +pub(crate) use api::*; + +pub mod tui; +pub(crate) use tui::*; + +testmod! { test } + +pub(crate) use clap::{self, Parser}; + +pub use better_panic; +pub(crate) use better_panic::{Settings, Verbosity}; + +pub use atomic_float; +pub(crate) use atomic_float::*; + +pub(crate) use std::sync::{Arc, Mutex, RwLock}; +pub(crate) use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize}; +pub(crate) use std::collections::BTreeMap; +pub(crate) use std::marker::PhantomData; +pub(crate) use std::thread::{spawn, JoinHandle}; +pub(crate) use std::path::PathBuf; +pub(crate) use std::ffi::OsString; +pub(crate) use std::time::Duration; +pub(crate) use std::io::{Stdout, stdout}; +pub(crate) use std::error::Error; +pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem}; +pub(crate) use std::cmp::{Ord, Eq, PartialEq}; +pub(crate) use std::fmt::{Debug, Display}; + +pub use crossterm; +pub(crate) use crossterm::{ExecutableCommand}; +pub(crate) use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}; +pub(crate) use crossterm::event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState}; + +pub use ratatui; +pub(crate) use ratatui::{ + prelude::{Style, Color, Buffer}, + style::{Stylize, Modifier}, + backend::{Backend, CrosstermBackend, ClearType} +}; + +pub use jack; +pub(crate) use jack::{ + contrib::ClosureProcessHandler, + Client, ProcessScope, Control, CycleTimes, + Port, PortSpec, MidiIn, MidiOut, AudioIn, AudioOut, Unowned, + Transport, TransportState, MidiIter, RawMidi, +}; + +pub use midly; +pub(crate) use midly::{ + Smf, + MidiMessage, + TrackEventKind, + live::LiveEvent, + num::u7 +}; + +pub use palette; +pub(crate) use palette::{ + *, + convert::*, + okhsl::* +}; diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index 75e7c016..4534c36a 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -1,10 +1,21 @@ use crate::*; -mod tui_input; pub(crate) use tui_input::*; -mod tui_style; pub(crate) use tui_style::*; -mod tui_theme; pub(crate) use tui_theme::*; -mod tui_output; pub(crate) use tui_output::*; -mod tui_border; pub(crate) use tui_border::*; +mod tui_input; +pub(crate) use tui_input::*; +pub use tui_input::TuiInput; + +mod tui_output; +pub(crate) use tui_output::*; +pub use tui_output::TuiOutput; + +//////////////////////////////////////////////////////// + +mod tui_style; +pub(crate) use tui_style::*; +mod tui_theme; +pub(crate) use tui_theme::*; +mod tui_border; +pub(crate) use tui_border::*; //////////////////////////////////////////////////////// From 8cf42aff0b14ee8e294db1a648c74156b9f9177c Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 12:46:42 +0100 Subject: [PATCH 016/905] extract edn; build out more groovebox --- crates/tek/src/api/sampler.rs | 119 +++------------------------ crates/tek/src/api/scene.rs | 31 ------- crates/tek/src/core.rs | 1 - crates/tek/src/core/edn.rs | 15 ---- crates/tek/src/core/space.rs | 8 +- crates/tek/src/edn.rs | 123 ++++++++++++++++++++++++++++ crates/tek/src/lib.rs | 3 + crates/tek/src/tui/app_groovebox.rs | 32 ++++++-- crates/tek/src/tui/app_sampler.rs | 102 +++++++++++++---------- crates/tek/src/tui/app_sequencer.rs | 2 +- 10 files changed, 231 insertions(+), 205 deletions(-) delete mode 100644 crates/tek/src/core/edn.rs create mode 100644 crates/tek/src/edn.rs diff --git a/crates/tek/src/api/sampler.rs b/crates/tek/src/api/sampler.rs index 07855dbd..c81e28cc 100644 --- a/crates/tek/src/api/sampler.rs +++ b/crates/tek/src/api/sampler.rs @@ -1,18 +1,5 @@ use crate::*; -pub struct SamplerAudio { - model: Arc> -} - -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] -pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - /// The sampler plugin plays sounds. #[derive(Debug)] pub struct Sampler { @@ -37,6 +24,15 @@ pub struct Sample { pub rate: Option, } +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] +pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} + /// Load sample from WAV and assign to MIDI note. #[macro_export] macro_rules! sample { ($note:expr, $name:expr, $src:expr) => {{ @@ -49,66 +45,10 @@ pub struct Sample { } impl Sampler { - pub fn from_edn <'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { - let mut name = String::new(); - let mut dir = String::new(); - let mut samples = BTreeMap::new(); - 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::Str(n)) = map.get(&Edn::Key(":dir")) { - dir = String::from(*n); - } - }, - Edn::List(args) => match args.get(0) { - Some(Edn::Symbol("sample")) => { - let (midi, sample) = Sample::from_edn(jack, &dir, &args[1..])?; - if let Some(midi) = midi { - samples.insert(midi, sample); - } else { - panic!("sample without midi binding: {}", sample.read().unwrap().name); - } - }, - _ => panic!("unexpected in sampler {name}: {args:?}") - }, - _ => panic!("unexpected in sampler {name}: {edn:?}") - }); - let midi_in = jack.read().unwrap().client().register_port("in", MidiIn::default())?; - Ok(Sampler { - jack: jack.clone(), - name: name.into(), - mapped: samples, - unmapped: Default::default(), - voices: Default::default(), - buffer: Default::default(), - midi_in: midi_in, - audio_outs: vec![], - output_gain: 0. - }) - } -} - -impl From<&Arc>> for SamplerAudio { - fn from (model: &Arc>) -> Self { - Self { model: model.clone() } - } -} - -audio!(|self: SamplerAudio, _client, scope|{ - self.process_midi_in(scope); - self.clear_output_buffer(); - self.process_audio_out(scope); - self.write_output_buffer(scope); - Control::Continue -}); - -impl SamplerAudio { /// Create [Voice]s from [Sample]s in response to MIDI input. pub fn process_midi_in (&mut self, scope: &ProcessScope) { - let Sampler { midi_in, mapped, voices, .. } = &*self.model.read().unwrap(); + let Sampler { midi_in, mapped, voices, .. } = self; for RawMidi { time, bytes } in midi_in.iter(scope) { if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { if let MidiMessage::NoteOn { ref key, ref vel } = message { @@ -122,14 +62,14 @@ impl SamplerAudio { /// Zero the output buffer. pub fn clear_output_buffer (&mut self) { - for buffer in self.model.write().unwrap().buffer.iter_mut() { + for buffer in self.buffer.iter_mut() { buffer.fill(0.0); } } /// Mix all currently playing samples into the output. pub fn process_audio_out (&mut self, scope: &ProcessScope) { - let Sampler { ref mut buffer, voices, output_gain, .. } = &mut*self.model.write().unwrap(); + let Sampler { ref mut buffer, voices, output_gain, .. } = self; let channel_count = buffer.len(); voices.write().unwrap().retain_mut(|voice|{ for index in 0..scope.n_frames() as usize { @@ -151,7 +91,7 @@ impl SamplerAudio { /// Write output buffer to output ports. pub fn write_output_buffer (&mut self, scope: &ProcessScope) { - let Sampler { ref mut audio_outs, buffer, .. } = &mut*self.model.write().unwrap(); + let Sampler { ref mut audio_outs, buffer, .. } = self; for (i, port) in audio_outs.iter_mut().enumerate() { let buffer = &buffer[i]; for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() { @@ -162,7 +102,6 @@ impl SamplerAudio { } - impl Sample { pub fn new (name: &str, start: usize, end: usize, channels: Vec>) -> Self { Self { name: name.to_string(), start, end, channels, rate: None } @@ -175,38 +114,6 @@ impl Sample { velocity: velocity.as_int() as f32 / 127.0, } } - pub fn from_edn <'e> (jack: &Arc>, dir: &str, args: &[Edn<'e>]) -> Usually<(Option, Arc>)> { - let mut name = String::new(); - let mut file = String::new(); - let mut midi = None; - let mut start = 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::Str(f)) = map.get(&Edn::Key(":file")) { - file = String::from(*f); - } - if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) { - start = *i as usize; - } - if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) { - midi = Some(u7::from(*m as u8)); - } - }, - _ => panic!("unexpected in sample {name}"), - }); - let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?; - Ok((midi, Arc::new(RwLock::new(Self { - name: name.into(), - start, - end, - channels: data, - rate: None - })))) - } - /// Read WAV from file pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { let mut channels: Vec> = vec![]; diff --git a/crates/tek/src/api/scene.rs b/crates/tek/src/api/scene.rs index 662569bc..2d464290 100644 --- a/crates/tek/src/api/scene.rs +++ b/crates/tek/src/api/scene.rs @@ -94,34 +94,3 @@ pub trait ArrangerSceneApi: Sized { } } - -//impl ArrangerScene { - - ////TODO - ////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:?}") - ////}); - ////Ok(ArrangerScene { - ////name: Arc::new(name.unwrap_or("").to_string().into()), - ////color: ItemColor::random(), - ////clips, - ////}) - ////} -//} diff --git a/crates/tek/src/core.rs b/crates/tek/src/core.rs index a0e94939..caacfa57 100644 --- a/crates/tek/src/core.rs +++ b/crates/tek/src/core.rs @@ -1,7 +1,6 @@ pub(crate) mod audio; pub(crate) use audio::*; pub(crate) mod color; pub(crate) use color::*; pub(crate) mod command; pub(crate) use command::*; -pub(crate) mod edn; pub(crate) use edn::*; pub(crate) mod engine; pub(crate) use engine::*; pub(crate) mod focus; pub(crate) use focus::*; pub(crate) mod input; pub(crate) use input::*; diff --git a/crates/tek/src/core/edn.rs b/crates/tek/src/core/edn.rs deleted file mode 100644 index b2c50a53..00000000 --- a/crates/tek/src/core/edn.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub use clojure_reader::edn::Edn; -//pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError}; - -/// EDN parsing helper. -#[macro_export] macro_rules! edn { - ($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => { - match $edn { $($pat => $expr),* } - }; - ($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => { - for $edn in $args { - edn!($edn { $($pat => $expr),* }) - } - }; -} - diff --git a/crates/tek/src/core/space.rs b/crates/tek/src/core/space.rs index 6942f437..490dcc60 100644 --- a/crates/tek/src/core/space.rs +++ b/crates/tek/src/core/space.rs @@ -76,8 +76,12 @@ pub trait Area: Copy { #[inline] fn lrtb (&self) -> [N;4] { [self.x(), self.x2(), self.y(), self.y2()] } #[inline] fn push_x (&self, x: N) -> [N;4] { [self.x() + x, self.y(), self.w(), self.h()] } #[inline] fn push_y (&self, y: N) -> [N;4] { [self.x(), self.y() + y, self.w(), self.h()] } - #[inline] fn shrink_x (&self, x: N) -> [N;4] { [self.x(), self.y(), self.w() - x, self.h()] } - #[inline] fn shrink_y (&self, y: N) -> [N;4] { [self.x(), self.y(), self.w(), self.h() - y] } + #[inline] fn shrink_x (&self, x: N) -> [N;4] { + [self.x(), self.y(), self.w().minus(x), self.h()] + } + #[inline] fn shrink_y (&self, y: N) -> [N;4] { + [self.x(), self.y(), self.w(), self.h().minus(y)] + } #[inline] fn set_w (&self, w: N) -> [N;4] { [self.x(), self.y(), w, self.h()] } #[inline] fn set_h (&self, h: N) -> [N;4] { [self.x(), self.y(), self.w(), h] } #[inline] fn clip_h (&self, h: N) -> [N;4] { diff --git a/crates/tek/src/edn.rs b/crates/tek/src/edn.rs new file mode 100644 index 00000000..f0c4d6ad --- /dev/null +++ b/crates/tek/src/edn.rs @@ -0,0 +1,123 @@ +use crate::*; +pub use clojure_reader::edn::Edn; +//pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError}; + +/// EDN parsing helper. +#[macro_export] macro_rules! edn { + ($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => { + match $edn { $($pat => $expr),* } + }; + ($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => { + for $edn in $args { + edn!($edn { $($pat => $expr),* }) + } + }; +} + +impl crate::Sampler { + pub fn from_edn <'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { + let mut name = String::new(); + let mut dir = String::new(); + let mut samples = BTreeMap::new(); + 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::Str(n)) = map.get(&Edn::Key(":dir")) { + dir = String::from(*n); + } + }, + Edn::List(args) => match args.get(0) { + Some(Edn::Symbol("sample")) => { + let (midi, sample) = Sample::from_edn(jack, &dir, &args[1..])?; + if let Some(midi) = midi { + samples.insert(midi, sample); + } else { + panic!("sample without midi binding: {}", sample.read().unwrap().name); + } + }, + _ => panic!("unexpected in sampler {name}: {args:?}") + }, + _ => panic!("unexpected in sampler {name}: {edn:?}") + }); + let midi_in = jack.read().unwrap().client().register_port("in", MidiIn::default())?; + Ok(Sampler { + jack: jack.clone(), + name: name.into(), + mapped: samples, + unmapped: Default::default(), + voices: Default::default(), + buffer: Default::default(), + midi_in: midi_in, + audio_outs: vec![], + output_gain: 0. + }) + } +} + +impl crate::Sample { + pub fn from_edn <'e> (jack: &Arc>, dir: &str, args: &[Edn<'e>]) -> Usually<(Option, Arc>)> { + let mut name = String::new(); + let mut file = String::new(); + let mut midi = None; + let mut start = 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::Str(f)) = map.get(&Edn::Key(":file")) { + file = String::from(*f); + } + if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) { + start = *i as usize; + } + if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) { + midi = Some(u7::from(*m as u8)); + } + }, + _ => panic!("unexpected in sample {name}"), + }); + let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?; + Ok((midi, Arc::new(RwLock::new(Self { + name: name.into(), + start, + end, + channels: data, + rate: None + })))) + } +} + + +//impl ArrangerScene { + + ////TODO + ////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:?}") + ////}); + ////Ok(ArrangerScene { + ////name: Arc::new(name.unwrap_or("").to_string().into()), + ////color: ItemColor::random(), + ////clips, + ////}) + ////} +//} diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs index a6378d05..f27561eb 100644 --- a/crates/tek/src/lib.rs +++ b/crates/tek/src/lib.rs @@ -31,6 +31,9 @@ pub(crate) use api::*; pub mod tui; pub(crate) use tui::*; +pub mod edn; +pub(crate) use edn::*; + testmod! { test } pub(crate) use clap::{self, Parser}; diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index 84542517..654198f3 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -10,7 +10,8 @@ impl TryFrom<&Arc>> for GrooveboxTui { Ok(Self { sequencer, sampler: SamplerTui::try_from(jack)?, - split: 16 + split: 16, + focus: GrooveboxFocus::Sequencer, }) } } @@ -19,24 +20,39 @@ pub struct GrooveboxTui { pub sequencer: SequencerTui, pub sampler: SamplerTui, pub split: u16, + pub focus: GrooveboxFocus } +pub enum GrooveboxFocus { + Sequencer, + Sampler +} + +audio!(|self:GrooveboxTui,_client,_process|Control::Continue); + +render!(|self:GrooveboxTui|Bsp::n( + Fixed::h(2, SequencerStatusBar::from(&self.sequencer)), + Fill::h(Bsp::s(&self.sequencer, &self.sampler)), +)); + pub enum GrooveboxCommand { Sequencer(SequencerCommand), Sampler(SamplerCommand), } -render!(|self:GrooveboxTui|Bsp::n( - Fixed::h(2, SequencerStatusBar::from(&self.sequencer)), - Bsp::s(Fixed::h(self.split, &self.sequencer), &self.sampler), -)); -audio!(|self:GrooveboxTui,_client,_process|Control::Continue); handle!(|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input)); + input_to_command!(GrooveboxCommand: |state:GrooveboxTui,input|match input.event() { // load sample - key_pat!(Char('l')) => GrooveboxCommand::Sampler(SamplerCommand::Import), - _ => GrooveboxCommand::Sequencer(SequencerCommand::input_to_command(&state.sequencer, input)?), + key_pat!(Char('l')) => GrooveboxCommand::Sampler(SamplerCommand::Import(FileBrowserCommand::Begin)), + _ => match state.focus { + GrooveboxFocus::Sequencer => + GrooveboxCommand::Sequencer(SequencerCommand::input_to_command(&state.sequencer, input)?), + GrooveboxFocus::Sampler => + GrooveboxCommand::Sampler(SamplerCommand::input_to_command(&state.sampler, input)?), + } }); + command!(|self:GrooveboxCommand,state:GrooveboxTui|match self { GrooveboxCommand::Sequencer(command) => command.execute(&mut state.sequencer)?.map(GrooveboxCommand::Sequencer), diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index 005a023b..d61db7aa 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -9,18 +9,32 @@ use symphonia::core::audio::SampleBuffer; use symphonia::default::get_codecs; pub enum SamplerCommand { - Import + Import(FileBrowserCommand), + SetName(String), + SetNote(u7, Arc>), + SetGain(f32), + NoteOn(u7, u7), + NoteOff(u7) } -input_to_command!(SamplerCommand: |state:SamplerTui,input|match input.event() { +input_to_command!(SamplerCommand:|state:SamplerTui,input|match state.mode { + Some(SamplerMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?), + _ => return None +}); +input_to_command!(FileBrowserCommand:|state:SamplerTui,input|match input { _ => return None }); command!(|self:SamplerCommand,state:SamplerTui|match self { - SamplerCommand::Import => { + SamplerCommand::Import(FileBrowserCommand::Begin) => { + let voices = &state.state.voices; + let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); None }, _ => todo!() }); - +command!(|self:FileBrowserCommand,state:SamplerTui|match self { + _ => todo!() +}); impl TryFrom<&Arc>> for SamplerTui { type Error = Box; @@ -31,10 +45,9 @@ impl TryFrom<&Arc>> for SamplerTui { jack.read().unwrap().client().register_port("outR", AudioOut::default())?, ]; Ok(Self { - focus: SamplerFocus::_TODO, cursor: (0, 0), editing: None, - modal: Default::default(), + mode: None, state: Sampler { jack: jack.clone(), name: "Sampler".into(), @@ -51,47 +64,54 @@ impl TryFrom<&Arc>> for SamplerTui { } pub struct SamplerTui { - pub focus: SamplerFocus, pub state: Sampler, pub cursor: (usize, usize), pub editing: Option>>, - pub modal: Arc>>>, + pub mode: Option, } -/// Sections that may be focused -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum SamplerFocus { - _TODO +enum SamplerMode { + // Load sample from path + Import(usize, FileBrowser), } -audio!(|self: SamplerTui, _client, _scope|Control::Continue); -render!(|self: SamplerTui|{ - Fill::wh(lay!([ - - Fill::wh(render(|to|{ // border - let [x, y, w, h] = to.area(); - let green = Some(Style::default().fg(Color::Green)); - to.blit(&"🭚", x, y, green); - to.blit(&"🭥", x + w.saturating_sub(1), y, green); - to.blit(&"🬿", x, y + h.saturating_sub(1), green); - to.blit(&"🭊", x + w.saturating_sub(1), y + h.saturating_sub(1), green); - Ok(()) - })), - - col!([ - Tui::push_x(2, row!([ - Tui::bold(true, "Sampler"), "|Voices: ", - &format!("{}", self.state.voices.read().unwrap().len()), - ])), - Tui::either(self.state.unmapped.len() > 0, - col!((index, sample) in self.state.unmapped.iter().enumerate() => - { &format!("| Unmapped #{index}") }), "· No unmapped samples."), - Tui::either(self.state.mapped.len() > 0, - col!((index, sample) in self.state.unmapped.iter().enumerate() => - { &format!("| Mapped #{index}") }), "· No mapped samples."), - ]) - ])) +audio!(|self: SamplerTui, _client, scope|{ + self.state.process_midi_in(scope); + self.state.clear_output_buffer(); + self.state.process_audio_out(scope); + self.state.write_output_buffer(scope); + Control::Continue }); +render!(|self: SamplerTui|Tui::min_y(10, Fill::wh(lay!([ + + Fill::wh(render(|to|{ // border + let [x, y, w, h] = to.area(); + let green = Some(Style::default().fg(Color::Green)); + to.blit(&"🭚", x, y, green); + to.blit(&"🭥", x + w.saturating_sub(1), y, green); + to.blit(&"🬿", x, y + h.saturating_sub(1), green); + to.blit(&"🭊", x + w.saturating_sub(1), y + h.saturating_sub(1), green); + Ok(()) + })), + + col!(|add|{ + add(&Tui::push_x(2, row!([ + Tui::bold(true, "Sampler"), "|Voices: ", + &format!("{}", self.state.voices.read().unwrap().len()), + ])))?; + if let Some(SamplerMode::Import(_, browser)) = self.mode.as_ref() { + add(&browser) + } else { + add(&row!([ + " ", + col!(note in 35..=42 => { + &format!("{note:>3} ---------------- +0.0dB") + }), + ])) + } + }), + +])))); handle!(|self:SamplerTui,from|{ let cursor = &mut self.cursor; @@ -112,11 +132,11 @@ handle!(|self:SamplerTui,from|{ }, key_pat!(KeyCode::Char('a')) => { let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); unmapped.push(sample); }, key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); }, key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { self.editing = Some(sample.clone()); diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 0dcc3f0d..44e247f1 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -154,7 +154,7 @@ render!(|self: SequencerTui|{ PhraseSelector::play_phrase(&self.player), PhraseSelector::next_phrase(&self.player), ]), transport]); - with_size(with_status(col!([ toolbar, editor, ]))) + Tui::min_y(15, with_size(with_status(col!([ toolbar, editor, ])))) }); audio!(|self:SequencerTui, client, scope|{ // Start profiling cycle From 0496ed6251d661fbcdeefd007dda5edceabdb565 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 13:11:28 +0100 Subject: [PATCH 017/905] add from_jack! --- crates/tek/src/api/jack.rs | 11 ++ crates/tek/src/layout/cond.rs | 6 + crates/tek/src/tui/app_groovebox.rs | 2 +- crates/tek/src/tui/app_sampler.rs | 169 +++++++++++++--------------- 4 files changed, 97 insertions(+), 91 deletions(-) diff --git a/crates/tek/src/api/jack.rs b/crates/tek/src/api/jack.rs index 99b0edcf..79a80d54 100644 --- a/crates/tek/src/api/jack.rs +++ b/crates/tek/src/api/jack.rs @@ -24,6 +24,17 @@ pub trait Audio: Send + Sync { } } +#[macro_export] macro_rules! from_jack { + (|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc>> for $Struct $(<$($L),*$($T),*>)? { + type Error = Box; + fn try_from ($jack: &Arc>) -> Usually { + Ok($cb) + } + } + }; +} + pub trait HasMidiIns { fn midi_ins (&self) -> &Vec>; fn midi_ins_mut (&mut self) -> &mut Vec>; diff --git a/crates/tek/src/layout/cond.rs b/crates/tek/src/layout/cond.rs index c4a6314d..7966f536 100644 --- a/crates/tek/src/layout/cond.rs +++ b/crates/tek/src/layout/cond.rs @@ -1,5 +1,11 @@ use crate::*; +pub enum Cond, B: Render> { + _Unused(E), + When(bool, A), + Either(bool, A, B) +} + impl> LayoutCond for R {} pub trait LayoutCond: Render + Sized { diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index 654198f3..72aadb16 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -11,7 +11,7 @@ impl TryFrom<&Arc>> for GrooveboxTui { sequencer, sampler: SamplerTui::try_from(jack)?, split: 16, - focus: GrooveboxFocus::Sequencer, + focus: GrooveboxFocus::Sampler, }) } } diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index d61db7aa..dd551f04 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -8,8 +8,44 @@ use symphonia::core::probe::Hint; use symphonia::core::audio::SampleBuffer; use symphonia::default::get_codecs; +pub struct SamplerTui { + pub state: Sampler, + pub cursor: (usize, usize), + pub editing: Option>>, + pub mode: Option, +} +pub enum SamplerMode { + // Load sample from path + Import(usize, FileBrowser), +} +from_jack!(|jack|SamplerTui{ + let midi_in = jack.read().unwrap().client().register_port("in", MidiIn::default())?; + let audio_outs = vec![ + jack.read().unwrap().client().register_port("outL", AudioOut::default())?, + jack.read().unwrap().client().register_port("outR", AudioOut::default())?, + ]; + Self { + cursor: (0, 0), + editing: None, + mode: None, + state: Sampler { + jack: jack.clone(), + name: "Sampler".into(), + mapped: BTreeMap::new(), + unmapped: vec![], + voices: Arc::new(RwLock::new(vec![])), + buffer: vec![vec![0.0;16384];2], + output_gain: 0.5, + midi_in, + audio_outs, + }, + } +}); +handle!(|self:SamplerTui,input|SamplerCommand::execute_with_state(self, input)); pub enum SamplerCommand { Import(FileBrowserCommand), + SelectNote(usize), + SelectField(usize), SetName(String), SetNote(u7, Arc>), SetGain(f32), @@ -18,7 +54,34 @@ pub enum SamplerCommand { } input_to_command!(SamplerCommand:|state:SamplerTui,input|match state.mode { Some(SamplerMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?), - _ => return None + _ => todo!() + //_ => match input.event() { + //key_pat!(KeyCode::Up) => state.cursor.0 = if state.cursor.0 == 0 { + //mapped.len() + unmapped.len() - 1 + //} else { + //state.cursor.0 - 1 + //}, + //key_pat!(KeyCode::Down) => { + //state.cursor.0 = (state.cursor.0 + 1) % (mapped.len() + unmapped.len()); + //}, + //key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { + //voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); + //}, + //key_pat!(KeyCode::Char('a')) => { + //let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + //self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + //unmapped.push(sample); + //}, + //key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { + //self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + //}, + //key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { + //self.editing = Some(sample.clone()); + //}, + //_ => { + //return Ok(None) + //} + //} }); input_to_command!(FileBrowserCommand:|state:SamplerTui,input|match input { _ => return None @@ -35,46 +98,23 @@ command!(|self:SamplerCommand,state:SamplerTui|match self { command!(|self:FileBrowserCommand,state:SamplerTui|match self { _ => todo!() }); - -impl TryFrom<&Arc>> for SamplerTui { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - let midi_in = jack.read().unwrap().client().register_port("in", MidiIn::default())?; - let audio_outs = vec![ - jack.read().unwrap().client().register_port("outL", AudioOut::default())?, - jack.read().unwrap().client().register_port("outR", AudioOut::default())?, - ]; - Ok(Self { - cursor: (0, 0), - editing: None, - mode: None, - state: Sampler { - jack: jack.clone(), - name: "Sampler".into(), - mapped: BTreeMap::new(), - unmapped: vec![], - voices: Arc::new(RwLock::new(vec![])), - buffer: vec![vec![0.0;16384];2], - output_gain: 0.5, - midi_in, - audio_outs, - }, - }) +impl SamplerTui { + /// Immutable reference to sample at cursor. + pub fn sample (&self) -> Option<&Arc>> { + for (i, sample) in self.state.mapped.values().enumerate() { + if i == self.cursor.0 { + return Some(sample) + } + } + for (i, sample) in self.state.unmapped.iter().enumerate() { + if i + self.state.mapped.len() == self.cursor.0 { + return Some(sample) + } + } + None } } -pub struct SamplerTui { - pub state: Sampler, - pub cursor: (usize, usize), - pub editing: Option>>, - pub mode: Option, -} - -enum SamplerMode { - // Load sample from path - Import(usize, FileBrowser), -} - audio!(|self: SamplerTui, _client, scope|{ self.state.process_midi_in(scope); self.state.clear_output_buffer(); @@ -82,6 +122,7 @@ audio!(|self: SamplerTui, _client, scope|{ self.state.write_output_buffer(scope); Control::Continue }); + render!(|self: SamplerTui|Tui::min_y(10, Fill::wh(lay!([ Fill::wh(render(|to|{ // border @@ -113,58 +154,6 @@ render!(|self: SamplerTui|Tui::min_y(10, Fill::wh(lay!([ ])))); -handle!(|self:SamplerTui,from|{ - let cursor = &mut self.cursor; - let unmapped = &mut self.state.unmapped; - let mapped = &self.state.mapped; - let voices = &self.state.voices; - match from.event() { - key_pat!(KeyCode::Up) => cursor.0 = if cursor.0 == 0 { - mapped.len() + unmapped.len() - 1 - } else { - cursor.0 - 1 - }, - key_pat!(KeyCode::Down) => { - cursor.0 = (cursor.0 + 1) % (mapped.len() + unmapped.len()); - }, - key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { - voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); - }, - key_pat!(KeyCode::Char('a')) => { - let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); - unmapped.push(sample); - }, - key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { - self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); - }, - key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { - self.editing = Some(sample.clone()); - }, - _ => { - return Ok(None) - } - } - Ok(Some(true)) -}); - -impl SamplerTui { - /// Immutable reference to sample at cursor. - pub fn sample (&self) -> Option<&Arc>> { - for (i, sample) in self.state.mapped.values().enumerate() { - if i == self.cursor.0 { - return Some(sample) - } - } - for (i, sample) in self.state.unmapped.iter().enumerate() { - if i + self.state.mapped.len() == self.cursor.0 { - return Some(sample) - } - } - None - } -} - pub struct AddSampleModal { exited: bool, dir: PathBuf, From efda18293d08230fca2ba1077079c3e27deb74c4 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 13:28:03 +0100 Subject: [PATCH 018/905] apply from_jack! --- crates/tek/src/api/jack.rs | 31 +- crates/tek/src/tui/app_arranger.rs | 569 +++++++++++----------------- crates/tek/src/tui/app_groovebox.rs | 36 +- crates/tek/src/tui/app_sampler.rs | 8 +- crates/tek/src/tui/app_sequencer.rs | 236 ++++-------- crates/tek/src/tui/app_transport.rs | 48 +-- crates/tek/src/tui/status_bar.rs | 176 ++++++++- 7 files changed, 522 insertions(+), 582 deletions(-) diff --git a/crates/tek/src/api/jack.rs b/crates/tek/src/api/jack.rs index 79a80d54..6ce07fcd 100644 --- a/crates/tek/src/api/jack.rs +++ b/crates/tek/src/api/jack.rs @@ -1,6 +1,18 @@ use crate::*; -/// Trait for things that have a JACK process callback. +/// Implement [TryFrom<&Arc>>]: create app state from wrapped JACK handle. +#[macro_export] macro_rules! from_jack { + (|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc>> for $Struct $(<$($L),*$($T),*>)? { + type Error = Box; + fn try_from ($jack: &Arc>) -> Usually { + Ok($cb) + } + } + }; +} + +/// Trait for thing that has a JACK process callback. pub trait Audio: Send + Sync { fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { Control::Continue @@ -16,6 +28,7 @@ pub trait Audio: Send + Sync { } } +/// Implement [Audio]: provide JACK callbacks. #[macro_export] macro_rules! audio { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { @@ -24,17 +37,7 @@ pub trait Audio: Send + Sync { } } -#[macro_export] macro_rules! from_jack { - (|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc>> for $Struct $(<$($L),*$($T),*>)? { - type Error = Box; - fn try_from ($jack: &Arc>) -> Usually { - Ok($cb) - } - } - }; -} - +/// Trait for thing that may receive MIDI. pub trait HasMidiIns { fn midi_ins (&self) -> &Vec>; fn midi_ins_mut (&mut self) -> &mut Vec>; @@ -43,13 +46,15 @@ pub trait HasMidiIns { } } +/// Trait for thing that may output MIDI. pub trait HasMidiOuts { fn midi_outs (&self) -> &Vec>; fn midi_outs_mut (&mut self) -> &mut Vec>; - fn midi_note (&mut self) -> &mut Vec; fn has_midi_outs (&self) -> bool { self.midi_outs().len() > 0 } + /// Buffer for serializing a MIDI event. FIXME rename + fn midi_note (&mut self) -> &mut Vec; } //////////////////////////////////////////////////////////////////////////////////// diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index b2c8c6d4..287b659c 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -2,35 +2,6 @@ use crate::*; use crate::api::ArrangerTrackCommand; use crate::api::ArrangerSceneCommand; -impl TryFrom<&Arc>> for ArrangerTui { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - Ok(Self { - jack: jack.clone(), - clock: ClockModel::from(jack), - phrases: PhraseListModel::default(), - editor: PhraseEditorModel::default(), - selected: ArrangerSelection::Clip(0, 0), - scenes: vec![], - tracks: vec![], - color: TuiTheme::bg().into(), - history: vec![], - mode: ArrangerMode::Vertical(2), - name: Arc::new(RwLock::new(String::new())), - size: Measure::new(), - cursor: (0, 0), - splits: [20, 20], - entered: false, - menu_bar: None, - status_bar: None, - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - focus: FocusState::Entered(ArrangerFocus::Transport(TransportFocus::PlayPause)), - }) - } -} - /// Root view for standalone `tek_arranger` pub struct ArrangerTui { pub jack: Arc>, @@ -55,7 +26,29 @@ pub struct ArrangerTui { pub focus: FocusState, pub perf: PerfModel, } - +from_jack!(|jack| ArrangerTui Self { + jack: jack.clone(), + clock: ClockModel::from(jack), + phrases: PhraseListModel::default(), + editor: PhraseEditorModel::default(), + selected: ArrangerSelection::Clip(0, 0), + scenes: vec![], + tracks: vec![], + color: TuiTheme::bg().into(), + history: vec![], + mode: ArrangerMode::Vertical(2), + name: Arc::new(RwLock::new(String::new())), + size: Measure::new(), + cursor: (0, 0), + splits: [20, 20], + entered: false, + menu_bar: None, + status_bar: None, + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + focus: FocusState::Entered(ArrangerFocus::Transport(TransportFocus::PlayPause)), +}); has_clock!(|self:ArrangerTui|&self.clock); has_phrases!(|self:ArrangerTui|self.phrases.phrases); has_editor!(|self:ArrangerTui|self.editor); @@ -146,7 +139,6 @@ impl HasPhraseList for ArrangerTui { self.phrases.phrase.load(Ordering::Relaxed) } } - #[derive(Clone, Debug)] pub enum ArrangerCommand { Focus(FocusCommand), @@ -163,7 +155,6 @@ pub enum ArrangerCommand { Phrases(PhrasesCommand), Editor(PhraseCommand), } - impl Command for ArrangerCommand { fn execute (self, state: &mut ArrangerTui) -> Perhaps { use ArrangerCommand::*; @@ -184,28 +175,24 @@ impl Command for ArrangerCommand { }) } } - impl Command for ArrangerSceneCommand { fn execute (self, _state: &mut ArrangerTui) -> Perhaps { //todo!(); Ok(None) } } - impl Command for ArrangerTrackCommand { fn execute (self, _state: &mut ArrangerTui) -> Perhaps { //todo!(); Ok(None) } } - impl Command for ArrangerClipCommand { fn execute (self, _state: &mut ArrangerTui) -> Perhaps { //todo!(); Ok(None) } } - pub trait ArrangerControl: TransportControl { fn selected (&self) -> ArrangerSelection; fn selected_mut (&mut self) -> &mut ArrangerSelection; @@ -214,7 +201,6 @@ pub trait ArrangerControl: TransportControl { fn toggle_loop (&mut self); fn randomize_color (&mut self); } - impl ArrangerControl for ArrangerTui { fn selected (&self) -> ArrangerSelection { self.selected @@ -272,8 +258,6 @@ impl InputToCommand for ArrangerCommand { .or_else(||to_focus_command(input).map(ArrangerCommand::Focus)) } } - - fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option { use ArrangerCommand as Cmd; use KeyCode::Char; @@ -318,7 +302,6 @@ fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option Option { use KeyCode::{Char, Down, Right, Delete}; use ArrangerCommand as Cmd; @@ -335,7 +318,6 @@ fn to_arranger_mix_command (input: &TuiInput) -> Option { _ => return None }) } - fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option { use KeyCode::{Char, Down, Left, Right, Delete}; use ArrangerCommand as Cmd; @@ -354,7 +336,6 @@ fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option return None }) } - fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { use KeyCode::{Char, Up, Down, Right, Enter, Delete}; use ArrangerCommand as Cmd; @@ -374,7 +355,6 @@ fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option return None }) } - fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option { use KeyCode::{Char, Up, Down, Left, Right, Delete}; use ArrangerCommand as Cmd; @@ -396,7 +376,6 @@ fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option return None }) } - impl TransportControl for ArrangerTui { fn transport_focused (&self) -> Option { match self.focus.inner() { @@ -440,7 +419,7 @@ impl From<&ArrangerTui> for Option { } } -impl_focus!(ArrangerTui ArrangerFocus [ +impl_focus!(ArrangerTui ArrangerFocus [ //&[ //Menu, //Menu, @@ -482,116 +461,6 @@ pub enum ArrangerStatus { PhraseEdit, } -impl StatusBar for ArrangerStatus { - type State = (ArrangerFocus, ArrangerSelection, bool); - fn hotkey_fg () -> Color where Self: Sized { - TuiTheme::HOTKEY_FG - } - fn update (&mut self, (focused, selected, entered): &Self::State) { - *self = match focused { - //ArrangerFocus::Menu => { todo!() }, - ArrangerFocus::Transport(_) => ArrangerStatus::Transport, - ArrangerFocus::Arranger => match selected { - ArrangerSelection::Mix => ArrangerStatus::ArrangerMix, - ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack, - ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene, - ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip, - }, - ArrangerFocus::Phrases => ArrangerStatus::PhrasePool, - ArrangerFocus::PhraseEditor => match entered { - true => ArrangerStatus::PhraseEdit, - false => ArrangerStatus::PhraseView, - }, - } - } -} - -render!(|self: ArrangerStatus|{ - - let label = match self { - Self::Transport => "TRANSPORT", - Self::ArrangerMix => "PROJECT", - Self::ArrangerTrack => "TRACK", - Self::ArrangerScene => "SCENE", - Self::ArrangerClip => "CLIP", - Self::PhrasePool => "SEQ LIST", - Self::PhraseView => "VIEW SEQ", - Self::PhraseEdit => "EDIT SEQ", - }; - - let status_bar_bg = TuiTheme::status_bar_bg(); - - let mode_bg = TuiTheme::mode_bg(); - let mode_fg = TuiTheme::mode_fg(); - let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} ")))); - - let commands = match self { - Self::ArrangerMix => Self::command(&[ - ["", "c", "olor"], - ["", "<>", "resize"], - ["", "+-", "zoom"], - ["", "n", "ame/number"], - ["", "Enter", " stop all"], - ]), - Self::ArrangerClip => Self::command(&[ - ["", "g", "et"], - ["", "s", "et"], - ["", "a", "dd"], - ["", "i", "ns"], - ["", "d", "up"], - ["", "e", "dit"], - ["", "c", "olor"], - ["re", "n", "ame"], - ["", ",.", "select"], - ["", "Enter", " launch"], - ]), - Self::ArrangerTrack => Self::command(&[ - ["re", "n", "ame"], - ["", ",.", "resize"], - ["", "<>", "move"], - ["", "i", "nput"], - ["", "o", "utput"], - ["", "m", "ute"], - ["", "s", "olo"], - ["", "Del", "ete"], - ["", "Enter", " stop"], - ]), - Self::ArrangerScene => Self::command(&[ - ["re", "n", "ame"], - ["", "Del", "ete"], - ["", "Enter", " launch"], - ]), - Self::PhrasePool => Self::command(&[ - ["", "a", "ppend"], - ["", "i", "nsert"], - ["", "d", "uplicate"], - ["", "Del", "ete"], - ["", "c", "olor"], - ["re", "n", "ame"], - ["leng", "t", "h"], - ["", ",.", "move"], - ["", "+-", "resize view"], - ]), - Self::PhraseView => Self::command(&[ - ["", "enter", " edit"], - ["", "arrows/pgup/pgdn", " scroll"], - ["", "+=", "zoom"], - ]), - Self::PhraseEdit => Self::command(&[ - ["", "esc", " exit"], - ["", "a", "ppend"], - ["", "s", "et"], - ["", "][", "length"], - ["", "+-", "zoom"], - ]), - _ => Self::command(&[]) - }; - - //let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}")); - Tui::bg(status_bar_bg, Fill::w(row!([mode, commands]))) - -}); - /// Display mode of arranger #[derive(Clone, PartialEq)] pub enum ArrangerMode { @@ -912,201 +781,6 @@ render!(|self: ArrangerVerticalContent<'a>|Fixed::h( }) )); -pub fn arranger_content_horizontal ( - view: &ArrangerTui, -) -> impl Render + use<'_> { - todo!() -} - //let focused = view.arranger_focused(); - //let _tracks = view.tracks(); - //lay!( - //focused.then_some(Background(TuiTheme::border_bg())), - //row!( - //// name - //Widget::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 - //Widget::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 - //Widget::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 - //Widget::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 - //Widget::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 - //Widget::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 - //Widget::new(|_|{todo!()}, |to: &mut TuiOutput|{ - //let [x, y, _, height] = to.area(); - //let mut x2 = 0; - //Ok(for (scene_index, scene) in view.scenes().iter().enumerate() { - //let active_scene = view.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 = view.selected.track() == Some(i); - //if let Some(clip) = clip { - //let y2 = y + 2 + i as u16 * 2; - //let label = format!("{}", clip.read().unwrap().name); - //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; - //}) - //}), - //) - //) -//} - impl HasScenes for ArrangerTui { fn scenes (&self) -> &Vec { &self.scenes @@ -1307,3 +981,198 @@ pub enum ArrangerClipCommand { //Ok(None) //} //} + +pub fn arranger_content_horizontal ( + view: &ArrangerTui, +) -> impl Render + use<'_> { + todo!() +} + //let focused = view.arranger_focused(); + //let _tracks = view.tracks(); + //lay!( + //focused.then_some(Background(TuiTheme::border_bg())), + //row!( + //// name + //Widget::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 + //Widget::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 + //Widget::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 + //Widget::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 + //Widget::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 + //Widget::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 + //Widget::new(|_|{todo!()}, |to: &mut TuiOutput|{ + //let [x, y, _, height] = to.area(); + //let mut x2 = 0; + //Ok(for (scene_index, scene) in view.scenes().iter().enumerate() { + //let active_scene = view.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 = view.selected.track() == Some(i); + //if let Some(clip) = clip { + //let y2 = y + 2 + i as u16 * 2; + //let label = format!("{}", clip.read().unwrap().name); + //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; + //}) + //}), + //) + //) +//} diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index 72aadb16..fb9cd270 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -1,50 +1,37 @@ use crate::*; use super::*; use KeyCode::Char; - -impl TryFrom<&Arc>> for GrooveboxTui { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - let mut sequencer = SequencerTui::try_from(jack)?; - sequencer.status = false; - Ok(Self { - sequencer, - sampler: SamplerTui::try_from(jack)?, - split: 16, - focus: GrooveboxFocus::Sampler, - }) - } -} - pub struct GrooveboxTui { pub sequencer: SequencerTui, pub sampler: SamplerTui, pub split: u16, pub focus: GrooveboxFocus } - +from_jack!(|jack|GrooveboxTui { + let mut sequencer = SequencerTui::try_from(jack)?; + sequencer.status = false; + Self { + sequencer, + sampler: SamplerTui::try_from(jack)?, + split: 16, + focus: GrooveboxFocus::Sampler, + } +}); pub enum GrooveboxFocus { Sequencer, Sampler } - audio!(|self:GrooveboxTui,_client,_process|Control::Continue); - render!(|self:GrooveboxTui|Bsp::n( - Fixed::h(2, SequencerStatusBar::from(&self.sequencer)), + Fixed::h(2, SequencerStatus::from(&self.sequencer)), Fill::h(Bsp::s(&self.sequencer, &self.sampler)), )); - pub enum GrooveboxCommand { Sequencer(SequencerCommand), Sampler(SamplerCommand), } - handle!(|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input)); - input_to_command!(GrooveboxCommand: |state:GrooveboxTui,input|match input.event() { - // load sample - key_pat!(Char('l')) => GrooveboxCommand::Sampler(SamplerCommand::Import(FileBrowserCommand::Begin)), _ => match state.focus { GrooveboxFocus::Sequencer => GrooveboxCommand::Sequencer(SequencerCommand::input_to_command(&state.sequencer, input)?), @@ -52,7 +39,6 @@ input_to_command!(GrooveboxCommand: |state:GrooveboxTui,input|match input.e GrooveboxCommand::Sampler(SamplerCommand::input_to_command(&state.sampler, input)?), } }); - command!(|self:GrooveboxCommand,state:GrooveboxTui|match self { GrooveboxCommand::Sequencer(command) => command.execute(&mut state.sequencer)?.map(GrooveboxCommand::Sequencer), diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index dd551f04..9306db47 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -1,5 +1,6 @@ use crate::*; use super::*; +use KeyCode::Char; use std::fs::File; use symphonia::core::codecs::CODEC_TYPE_NULL; use symphonia::core::errors::Error; @@ -54,8 +55,11 @@ pub enum SamplerCommand { } input_to_command!(SamplerCommand:|state:SamplerTui,input|match state.mode { Some(SamplerMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?), - _ => todo!() - //_ => match input.event() { + _ => match input.event() { + // load sample + key_pat!(Char('l')) => Self::Import(FileBrowserCommand::Begin), + _ => return None + } //key_pat!(KeyCode::Up) => state.cursor.0 = if state.cursor.0 == 0 { //mapped.len() + unmapped.len() - 1 //} else { diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 44e247f1..aefd456a 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -3,33 +3,6 @@ use KeyCode::{Tab, BackTab, Char}; use SequencerCommand::*; use PhraseCommand::*; use PhrasePoolCommand::*; - -/// Create app state from JACK handle. -impl TryFrom<&Arc>> for SequencerTui { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - let clock = ClockModel::from(jack); - let phrase = Arc::new(RwLock::new(Phrase::new( - "New", true, 4 * clock.timebase.ppq.get() as usize, - None, Some(ItemColor::random().into()) - ))); - Ok(Self { - _jack: jack.clone(), - phrases: PhraseListModel::from(&phrase), - editor: PhraseEditorModel::from(&phrase), - player: PhrasePlayerModel::from((&clock, &phrase)), - clock, - size: Measure::new(), - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - show_pool: true, - status: true, - }) - - } -} - /// Root view for standalone `tek_sequencer`. pub struct SequencerTui { _jack: Arc>, @@ -44,17 +17,91 @@ pub struct SequencerTui { pub(crate) midi_buf: Vec>>, pub(crate) perf: PerfModel, } - -#[derive(Clone, Debug)] -pub enum SequencerCommand { +from_jack!(|jack|SequencerTui { + let clock = ClockModel::from(jack); + let phrase = Arc::new(RwLock::new(Phrase::new( + "New", true, 4 * clock.timebase.ppq.get() as usize, + None, Some(ItemColor::random().into()) + ))); + Self { + _jack: jack.clone(), + phrases: PhraseListModel::from(&phrase), + editor: PhraseEditorModel::from(&phrase), + player: PhrasePlayerModel::from((&clock, &phrase)), + clock, + size: Measure::new(), + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + show_pool: true, + status: true, + } +}); +render!(|self: SequencerTui|{ + let w = self.size.w(); + let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + let pool_w = if self.show_pool { phrase_w } else { 0 }; + let pool = Fill::h(Align::e(PhraseListView(&self.phrases))); + let with_pool = move|x|Tui::split_w(false, pool_w, pool, x); + let status = SequencerStatus::from(self); + let with_status = |x|Tui::split_n(false, if self.status { 2 } else { 0 }, status, x); + let with_editbar = |x|Tui::split_n(false, 3, PhraseEditStatus(&self.editor), x); + let with_size = |x|lay!([self.size, x]); + let editor = with_editbar(with_pool(Fill::wh(&self.editor))); + let color = self.player.play_phrase().as_ref().map(|(_,p)| + p.as_ref().map(|p|p.read().unwrap().color) + ).flatten().clone(); + let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); + let transport = Fixed::h(2, TransportView::from((self, color, true))); + let toolbar = row!([play, col!([ + PhraseSelector::play_phrase(&self.player), + PhraseSelector::next_phrase(&self.player), + ]), transport]); + Tui::min_y(15, with_size(with_status(col!([ toolbar, editor, ])))) +}); +audio!(|self:SequencerTui, 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 + } + // Update MIDI sequencer + if Control::Quit == PlayerAudio( + &mut self.player, &mut self.note_buf, &mut self.midi_buf + ).process(client, scope) { + return Control::Quit + } + // End profiling cycle + self.perf.update(t0, scope); + Control::Continue +}); +impl HasPhraseList for SequencerTui { + fn phrases_focused (&self) -> bool { + true + } + fn phrases_entered (&self) -> bool { + true + } + fn phrases_mode (&self) -> &Option { + &self.phrases.mode + } + fn phrase_index (&self) -> usize { + self.phrases.phrase.load(Ordering::Relaxed) + } +} +has_size!(|self:SequencerTui|&self.size); +has_clock!(|self:SequencerTui|&self.clock); +has_phrases!(|self:SequencerTui|self.phrases.phrases); +has_editor!(|self:SequencerTui|self.editor); +handle!(|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input)); +#[derive(Clone, Debug)] pub enum SequencerCommand { Clock(ClockCommand), Phrases(PhrasesCommand), Editor(PhraseCommand), Enqueue(Option>>), ShowPool(bool), } - -handle!(|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input)); input_to_command!(SequencerCommand: |state:SequencerTui,input|match input.event() { // Transport: Play/pause key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() { @@ -98,7 +145,9 @@ input_to_command!(SequencerCommand: |state:SequencerTui,input|match input.e }); command!(|self: SequencerCommand, state: SequencerTui|match self { Self::Phrases(cmd) => { - let mut default = |cmd: PhrasesCommand|cmd.execute(&mut state.phrases).map(|x|x.map(Phrases)); + let mut default = |cmd: PhrasesCommand|cmd + .execute(&mut state.phrases) + .map(|x|x.map(Phrases)); match cmd { // autoselect: automatically load selected phrase in editor PhrasesCommand::Select(_) => { @@ -131,124 +180,3 @@ command!(|self: SequencerCommand, state: SequencerTui|match self { None } }); - -has_size!(|self:SequencerTui|&self.size); -has_clock!(|self:SequencerTui|&self.clock); -has_phrases!(|self:SequencerTui|self.phrases.phrases); -has_editor!(|self:SequencerTui|self.editor); -render!(|self: SequencerTui|{ - let w = self.size.w(); - let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - let pool_w = if self.show_pool { phrase_w } else { 0 }; - let pool = Fill::h(Align::e(PhraseListView(&self.phrases))); - let with_pool = move|x|Tui::split_w(false, pool_w, pool, x); - let status = SequencerStatusBar::from(self); - let with_status = |x|Tui::split_n(false, if self.status { 2 } else { 0 }, status, x); - let with_editbar = |x|Tui::split_n(false, 3, PhraseEditStatus(&self.editor), x); - let with_size = |x|lay!([self.size, x]); - let editor = with_editbar(with_pool(Fill::wh(&self.editor))); - let color = self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten().clone(); - let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); - let transport = Fixed::h(2, TransportView::from((self, color, true))); - let toolbar = row!([play, col!([ - PhraseSelector::play_phrase(&self.player), - PhraseSelector::next_phrase(&self.player), - ]), transport]); - Tui::min_y(15, with_size(with_status(col!([ toolbar, editor, ])))) -}); -audio!(|self:SequencerTui, 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 - } - // Update MIDI sequencer - if Control::Quit == PlayerAudio( - &mut self.player, &mut self.note_buf, &mut self.midi_buf - ).process(client, scope) { - return Control::Quit - } - // End profiling cycle - self.perf.update(t0, scope); - Control::Continue -}); - -impl HasPhraseList for SequencerTui { - fn phrases_focused (&self) -> bool { - true - } - fn phrases_entered (&self) -> bool { - true - } - fn phrases_mode (&self) -> &Option { - &self.phrases.mode - } - fn phrase_index (&self) -> usize { - self.phrases.phrase.load(Ordering::Relaxed) - } -} - -/// Status bar for sequencer app -#[derive(Clone)] -pub struct SequencerStatusBar { - pub(crate) width: usize, - pub(crate) cpu: Option, - pub(crate) size: String, - pub(crate) res: String, - pub(crate) playing: bool, -} - -impl StatusBar for SequencerStatusBar { - type State = SequencerTui; - fn hotkey_fg () -> Color { - TuiTheme::HOTKEY_FG - } - fn update (&mut self, _: &SequencerTui) { - todo!() - } -} - -impl From<&SequencerTui> for SequencerStatusBar { - fn from (state: &SequencerTui) -> Self { - let samples = state.clock.chunk.load(Ordering::Relaxed); - let rate = state.clock.timebase.sr.get() as f64; - let buffer = samples as f64 / rate; - let width = state.size.w(); - Self { - width, - playing: state.clock.is_rolling(), - cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), - size: format!("{}x{}│", width, state.size.h()), - res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), - } - } -} - -render!(|self: SequencerStatusBar|Fixed::h(2, lay!([ - { - let single = |binding, command|row!([" ", col!([ - Tui::fg(TuiTheme::yellow(), binding), - command - ])]); - let double = |(b1, c1), (b2, c2)|col!([ - row!([" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,]), - row!([" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,]), - ]); - Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!([ - single("SPACE", "play/pause"), - double((" ✣", "cursor"), ("C-✣", "scroll"), ), - double(("a", "append"), ("s", "set note"),), - double((",.", "note"), ("<>", "triplet"), ), - double(("[]", "phrase"), ("{}", "order"), ), - double(("q", "enqueue"), ("e", "edit"), ), - ])) - }, - Fill::wh(Align::se({ - Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), row!([ - &self.cpu, - &self.res, - &self.size, - ])) - })), -]))); diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index 419c0656..535bad5a 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -3,7 +3,6 @@ use crate::api::ClockCommand::{Play, Pause, SetBpm, SetQuant, SetSync}; use TransportCommand::{Focus, Clock}; use FocusCommand::{Next, Prev}; use KeyCode::{Enter, Left, Right, Char}; - /// Transport clock app. pub struct TransportTui { pub jack: Arc>, @@ -12,21 +11,17 @@ pub struct TransportTui { pub cursor: (usize, usize), pub focus: FocusState, } - -/// Create app state from JACK handle. -impl TryFrom<&Arc>> for TransportTui { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - Ok(Self { - jack: jack.clone(), - clock: ClockModel::from(jack), - size: Measure::new(), - cursor: (0, 0), - focus: FocusState::Entered(TransportFocus::PlayPause) - }) - } -} - +from_jack!(|jack|TransportTui Self { + jack: jack.clone(), + clock: ClockModel::from(jack), + size: Measure::new(), + cursor: (0, 0), + focus: FocusState::Entered(TransportFocus::PlayPause) +}); +has_clock!(|self:TransportTui|&self.clock); +audio!(|self:TransportTui,client,scope|ClockAudio(self).process(client, scope)); +handle!(|self:TransportTui,from|TransportCommand::execute_with_state(self, from)); +render!(|self: TransportTui|TransportView::from((self, None, true))); impl std::fmt::Debug for TransportTui { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("TransportTui") @@ -36,12 +31,6 @@ impl std::fmt::Debug for TransportTui { .finish() } } - -has_clock!(|self:TransportTui|&self.clock); -audio!(|self:TransportTui,client,scope|ClockAudio(self).process(client, scope)); -handle!(|self:TransportTui,from|TransportCommand::execute_with_state(self, from)); -render!(|self: TransportTui|TransportView::from((self, None, true))); - pub struct TransportView { color: ItemPalette, focused: bool, @@ -187,21 +176,6 @@ impl FocusWrap for Option { } } -#[derive(Copy, Clone)] -pub struct TransportStatusBar; - -impl StatusBar for TransportStatusBar { - type State = (); - fn hotkey_fg () -> Color { - TuiTheme::HOTKEY_FG - } - fn update (&mut self, _: &()) { - todo!() - } -} - -render!(|self: TransportStatusBar|"todo"); - pub trait TransportControl: HasClock + { fn transport_focused (&self) -> Option; } diff --git a/crates/tek/src/tui/status_bar.rs b/crates/tek/src/tui/status_bar.rs index 926abf72..1835f563 100644 --- a/crates/tek/src/tui/status_bar.rs +++ b/crates/tek/src/tui/status_bar.rs @@ -1,6 +1,6 @@ use crate::*; -pub trait StatusBar: Render { +pub trait Bar: Render { type State: Send + Sync; fn hotkey_fg () -> Color where Self: Sized; fn update (&mut self, state: &Self::State) where Self: Sized; @@ -21,3 +21,177 @@ pub trait StatusBar: Render { Bsp::n(state.into(), content) } } + +/// Status bar for sequencer app +#[derive(Clone)] +pub struct SequencerStatus { + pub(crate) width: usize, + pub(crate) cpu: Option, + pub(crate) size: String, + pub(crate) res: String, + pub(crate) playing: bool, +} + +impl Bar for SequencerStatus { + type State = SequencerTui; + fn hotkey_fg () -> Color { + TuiTheme::HOTKEY_FG + } + fn update (&mut self, _: &SequencerTui) { + todo!() + } +} + +impl From<&SequencerTui> for SequencerStatus { + fn from (state: &SequencerTui) -> Self { + let samples = state.clock.chunk.load(Ordering::Relaxed); + let rate = state.clock.timebase.sr.get() as f64; + let buffer = samples as f64 / rate; + let width = state.size.w(); + Self { + width, + playing: state.clock.is_rolling(), + cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), + size: format!("{}x{}│", width, state.size.h()), + res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), + } + } +} + +render!(|self: SequencerStatus|Fixed::h(2, lay!([ + { + let single = |binding, command|row!([" ", col!([ + Tui::fg(TuiTheme::yellow(), binding), + command + ])]); + let double = |(b1, c1), (b2, c2)|col!([ + row!([" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,]), + row!([" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,]), + ]); + Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!([ + single("SPACE", "play/pause"), + double((" ✣", "cursor"), ("C-✣", "scroll"), ), + double(("a", "append"), ("s", "set note"),), + double((",.", "note"), ("<>", "triplet"), ), + double(("[]", "phrase"), ("{}", "order"), ), + double(("q", "enqueue"), ("e", "edit"), ), + ])) + }, + Fill::wh(Align::se({ + Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), row!([ + &self.cpu, + &self.res, + &self.size, + ])) + })), +]))); + +impl Bar for ArrangerStatus { + type State = (ArrangerFocus, ArrangerSelection, bool); + fn hotkey_fg () -> Color where Self: Sized { + TuiTheme::HOTKEY_FG + } + fn update (&mut self, (focused, selected, entered): &Self::State) { + *self = match focused { + //ArrangerFocus::Menu => { todo!() }, + ArrangerFocus::Transport(_) => ArrangerStatus::Transport, + ArrangerFocus::Arranger => match selected { + ArrangerSelection::Mix => ArrangerStatus::ArrangerMix, + ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack, + ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene, + ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip, + }, + ArrangerFocus::Phrases => ArrangerStatus::PhrasePool, + ArrangerFocus::PhraseEditor => match entered { + true => ArrangerStatus::PhraseEdit, + false => ArrangerStatus::PhraseView, + }, + } + } +} + +render!(|self: ArrangerStatus|{ + + let label = match self { + Self::Transport => "TRANSPORT", + Self::ArrangerMix => "PROJECT", + Self::ArrangerTrack => "TRACK", + Self::ArrangerScene => "SCENE", + Self::ArrangerClip => "CLIP", + Self::PhrasePool => "SEQ LIST", + Self::PhraseView => "VIEW SEQ", + Self::PhraseEdit => "EDIT SEQ", + }; + + let status_bar_bg = TuiTheme::status_bar_bg(); + + let mode_bg = TuiTheme::mode_bg(); + let mode_fg = TuiTheme::mode_fg(); + let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} ")))); + + let commands = match self { + Self::ArrangerMix => Self::command(&[ + ["", "c", "olor"], + ["", "<>", "resize"], + ["", "+-", "zoom"], + ["", "n", "ame/number"], + ["", "Enter", " stop all"], + ]), + Self::ArrangerClip => Self::command(&[ + ["", "g", "et"], + ["", "s", "et"], + ["", "a", "dd"], + ["", "i", "ns"], + ["", "d", "up"], + ["", "e", "dit"], + ["", "c", "olor"], + ["re", "n", "ame"], + ["", ",.", "select"], + ["", "Enter", " launch"], + ]), + Self::ArrangerTrack => Self::command(&[ + ["re", "n", "ame"], + ["", ",.", "resize"], + ["", "<>", "move"], + ["", "i", "nput"], + ["", "o", "utput"], + ["", "m", "ute"], + ["", "s", "olo"], + ["", "Del", "ete"], + ["", "Enter", " stop"], + ]), + Self::ArrangerScene => Self::command(&[ + ["re", "n", "ame"], + ["", "Del", "ete"], + ["", "Enter", " launch"], + ]), + Self::PhrasePool => Self::command(&[ + ["", "a", "ppend"], + ["", "i", "nsert"], + ["", "d", "uplicate"], + ["", "Del", "ete"], + ["", "c", "olor"], + ["re", "n", "ame"], + ["leng", "t", "h"], + ["", ",.", "move"], + ["", "+-", "resize view"], + ]), + Self::PhraseView => Self::command(&[ + ["", "enter", " edit"], + ["", "arrows/pgup/pgdn", " scroll"], + ["", "+=", "zoom"], + ]), + Self::PhraseEdit => Self::command(&[ + ["", "esc", " exit"], + ["", "a", "ppend"], + ["", "s", "et"], + ["", "][", "length"], + ["", "+-", "zoom"], + ]), + _ => Self::command(&[]) + }; + + //let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}")); + Tui::bg(status_bar_bg, Fill::w(row!([mode, commands]))) + +}); From 623fce73a40e7f2fd0415ce9be9eea78f6e3833b Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 13:46:07 +0100 Subject: [PATCH 019/905] remove HasPhraseList; 8470LOC --- crates/tek/src/tui/app_arranger.rs | 56 +++----------- crates/tek/src/tui/app_groovebox.rs | 8 +- crates/tek/src/tui/app_sampler.rs | 4 +- crates/tek/src/tui/app_sequencer.rs | 14 ---- crates/tek/src/tui/file_browser.rs | 116 +++------------------------- crates/tek/src/tui/phrase_length.rs | 78 ++++++++++--------- crates/tek/src/tui/phrase_list.rs | 90 +++++++++++++++------ 7 files changed, 133 insertions(+), 233 deletions(-) diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 287b659c..06e725ba 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -1,7 +1,6 @@ use crate::*; use crate::api::ArrangerTrackCommand; use crate::api::ArrangerSceneCommand; - /// Root view for standalone `tek_arranger` pub struct ArrangerTui { pub jack: Arc>, @@ -55,10 +54,8 @@ has_editor!(|self:ArrangerTui|self.editor); handle!(|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input)); render!(|self: ArrangerTui|{ let arranger_focused = self.arranger_focused(); - let transport_focused = if let ArrangerFocus::Transport(_) = self.focus.inner() { - true - } else { - false + let transport_focused = match self.focus.inner() { + ArrangerFocus::Transport(_) => true, _ => false }; let transport = TransportView::from((self, None, transport_focused)); let with_transport = move|x|col!([transport, x]); @@ -72,20 +69,8 @@ render!(|self: ArrangerTui|{ }; add(&self.size) }))); - with_transport(col!([ - Fixed::h(self.splits[0], lay!([ - arranger(), - Tui::push_x(1, Tui::fg( - TuiTheme::title_fg(arranger_focused), - format!("[{}] Arranger", if self.entered { - "■" - } else { - " " - }) - )) - ])), - Split::right(false, self.splits[1], PhraseListView(&self.phrases), &self.editor), - ])) + let with_pool = |x|Split::right(false, self.splits[1], PhraseListView(&self.phrases), x); + with_transport(col!([Fixed::h(self.splits[0], arranger()), with_pool(&self.editor),])) }); audio!(|self: ArrangerTui, client, scope|{ // Start profiling cycle @@ -124,23 +109,7 @@ audio!(|self: ArrangerTui, client, scope|{ self.perf.update(t0, scope); return Control::Continue }); - -impl HasPhraseList for ArrangerTui { - fn phrases_focused (&self) -> bool { - self.focused() == ArrangerFocus::Phrases - } - fn phrases_entered (&self) -> bool { - self.entered() && self.phrases_focused() - } - fn phrases_mode (&self) -> &Option { - &self.phrases.mode - } - fn phrase_index (&self) -> usize { - self.phrases.phrase.load(Ordering::Relaxed) - } -} -#[derive(Clone, Debug)] -pub enum ArrangerCommand { +#[derive(Clone, Debug)] pub enum ArrangerCommand { Focus(FocusCommand), Undo, Redo, @@ -384,8 +353,6 @@ impl TransportControl for ArrangerTui { } } } -has_clock!(|self:ArrangerTrack|self.player.clock()); -has_player!(|self:ArrangerTrack|self.player); /// Sections in the arranger app that may be focused #[derive(Copy, Clone, PartialEq, Eq, Debug)] @@ -809,8 +776,7 @@ impl HasScenes for ArrangerTui { } } -#[derive(Default, Debug, Clone)] -pub struct ArrangerScene { +#[derive(Default, Debug, Clone)] pub struct ArrangerScene { /// Name of scene pub(crate) name: Arc>, /// Clips in scene, one per track @@ -818,7 +784,6 @@ pub struct ArrangerScene { /// Identifying color of scene pub(crate) color: ItemColor, } - impl ArrangerSceneApi for ArrangerScene { fn name (&self) -> &Arc> { &self.name @@ -830,7 +795,6 @@ impl ArrangerSceneApi for ArrangerScene { self.color } } - impl HasTracks for ArrangerTui { fn tracks (&self) -> &Vec { &self.tracks @@ -839,7 +803,6 @@ impl HasTracks for ArrangerTui { &mut self.tracks } } - impl ArrangerTracksApi for ArrangerTui { fn track_add (&mut self, name: Option<&str>, color: Option) -> Usually<&mut ArrangerTrack> @@ -863,8 +826,7 @@ impl ArrangerTracksApi for ArrangerTui { } } -#[derive(Debug)] -pub struct ArrangerTrack { +#[derive(Debug)] pub struct ArrangerTrack { /// Name of track pub(crate) name: Arc>, /// Preferred width of track column @@ -874,7 +836,8 @@ pub struct ArrangerTrack { /// MIDI player state pub(crate) player: PhrasePlayerModel, } - +has_clock!(|self:ArrangerTrack|self.player.clock()); +has_player!(|self:ArrangerTrack|self.player); impl ArrangerTrackApi for ArrangerTrack { /// Name of track fn name (&self) -> &Arc> { @@ -893,7 +856,6 @@ impl ArrangerTrackApi for ArrangerTrack { self.color } } - #[derive(PartialEq, Clone, Copy, Debug)] /// Represents the current user selection in the arranger pub enum ArrangerSelection { diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index fb9cd270..b07691ef 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -33,10 +33,10 @@ pub enum GrooveboxCommand { handle!(|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input)); input_to_command!(GrooveboxCommand: |state:GrooveboxTui,input|match input.event() { _ => match state.focus { - GrooveboxFocus::Sequencer => - GrooveboxCommand::Sequencer(SequencerCommand::input_to_command(&state.sequencer, input)?), - GrooveboxFocus::Sampler => - GrooveboxCommand::Sampler(SamplerCommand::input_to_command(&state.sampler, input)?), + GrooveboxFocus::Sequencer => GrooveboxCommand::Sequencer( + SequencerCommand::input_to_command(&state.sequencer, input)?), + GrooveboxFocus::Sampler => GrooveboxCommand::Sampler( + SamplerCommand::input_to_command(&state.sampler, input)?), } }); command!(|self:GrooveboxCommand,state:GrooveboxTui|match self { diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index 9306db47..8753739e 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -54,7 +54,9 @@ pub enum SamplerCommand { NoteOff(u7) } input_to_command!(SamplerCommand:|state:SamplerTui,input|match state.mode { - Some(SamplerMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?), + Some(SamplerMode::Import(..)) => Self::Import( + FileBrowserCommand::input_to_command(state, input)? + ), _ => match input.event() { // load sample key_pat!(Char('l')) => Self::Import(FileBrowserCommand::Begin), diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index aefd456a..cf2a2fdb 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -76,20 +76,6 @@ audio!(|self:SequencerTui, client, scope|{ self.perf.update(t0, scope); Control::Continue }); -impl HasPhraseList for SequencerTui { - fn phrases_focused (&self) -> bool { - true - } - fn phrases_entered (&self) -> bool { - true - } - fn phrases_mode (&self) -> &Option { - &self.phrases.mode - } - fn phrase_index (&self) -> usize { - self.phrases.phrase.load(Ordering::Relaxed) - } -} has_size!(|self:SequencerTui|&self.size); has_clock!(|self:SequencerTui|&self.clock); has_phrases!(|self:SequencerTui|self.phrases.phrases); diff --git a/crates/tek/src/tui/file_browser.rs b/crates/tek/src/tui/file_browser.rs index 9d265a70..2cd49187 100644 --- a/crates/tek/src/tui/file_browser.rs +++ b/crates/tek/src/tui/file_browser.rs @@ -2,7 +2,6 @@ use crate::*; use KeyCode::{Up, Down, Right, Left, Enter, Esc, Char, Backspace}; use FileBrowserCommand::*; use super::phrase_list::PhraseListMode::{Import, Export}; - /// Browses for phrase to import/export #[derive(Debug, Clone)] pub struct FileBrowser { @@ -14,7 +13,16 @@ pub struct FileBrowser { pub scroll: usize, pub size: Measure } - +/// Commands supported by [FileBrowser] +#[derive(Debug, Clone, PartialEq)] +pub enum FileBrowserCommand { + Begin, + Cancel, + Confirm, + Select(usize), + Chdir(PathBuf), + Filter(String), +} render!(|self: FileBrowser|{ Stack::down(|add|{ let mut i = 0; @@ -34,7 +42,6 @@ render!(|self: FileBrowser|{ Ok(()) }) }); - impl FileBrowser { pub fn new (cwd: Option) -> Usually { let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; @@ -83,106 +90,3 @@ impl FileBrowser { Self::new(Some(self.path())) } } - -/// Commands supported by [FileBrowser] -#[derive(Debug, Clone, PartialEq)] -pub enum FileBrowserCommand { - Begin, - Cancel, - Confirm, - Select(usize), - Chdir(PathBuf), - Filter(String), -} - -command!(|self: FileBrowserCommand, state: PhraseListModel|{ - let mode = state.phrases_mode_mut(); - match mode { - Some(Import(index, ref mut browser)) => match self { - Cancel => { - *mode = None; - }, - Chdir(cwd) => { - *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); - }, - Select(index) => { - browser.index = index; - }, - Confirm => { - if browser.is_file() { - let index = *index; - let path = browser.path(); - *mode = None; - PhrasePoolCommand::Import(index, path).execute(state)?; - } else if browser.is_dir() { - *mode = Some(Import(*index, browser.chdir()?)); - } - }, - _ => todo!(), - }, - Some(PhraseListMode::Export(index, ref mut browser)) => match self { - Cancel => { - *mode = None; - }, - Chdir(cwd) => { - *mode = Some(PhraseListMode::Export(*index, FileBrowser::new(Some(cwd))?)); - }, - Select(index) => { - browser.index = index; - }, - _ => unreachable!() - }, - _ => unreachable!(), - }; - None -}); - -input_to_command!(FileBrowserCommand:|state:PhraseListModel,from|{ - if let Some(PhraseListMode::Import(_index, browser)) = state.phrases_mode() { - match from.event() { - key_pat!(Up) => Select( - browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1)) - ), - key_pat!(Down) => Select( - browser.index.saturating_add(1) % browser.len() - ), - key_pat!(Right) => Chdir(browser.cwd.clone()), - key_pat!(Left) => Chdir(browser.cwd.clone()), - key_pat!(Enter) => Confirm, - key_pat!(Char(_)) => { todo!() }, - key_pat!(Backspace) => { todo!() }, - key_pat!(Esc) => Self::Cancel, - _ => return None - } - } else if let Some(PhraseListMode::Export(_index, browser)) = state.phrases_mode() { - match from.event() { - key_pat!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())), - key_pat!(Down) => Select(browser.index.saturating_add(1) % browser.len()), - key_pat!(Right) => Chdir(browser.cwd.clone()), - key_pat!(Left) => Chdir(browser.cwd.clone()), - key_pat!(Enter) => Confirm, - key_pat!(Char(_)) => { todo!() }, - key_pat!(Backspace) => { todo!() }, - key_pat!(Esc) => Self::Cancel, - _ => return None - } - } else { - unreachable!() - } -}); - -input_to_command!(PhraseLengthCommand:|state:PhraseListModel,from|{ - if let Some(PhraseListMode::Length(_, length, _)) = state.phrases_mode() { - match from.event() { - key_pat!(Up) => Self::Inc, - key_pat!(Down) => Self::Dec, - key_pat!(Right) => Self::Next, - key_pat!(Left) => Self::Prev, - key_pat!(Enter) => Self::Set(*length), - key_pat!(Esc) => Self::Cancel, - _ => return None - } - } else { - unreachable!() - } -}); diff --git a/crates/tek/src/tui/phrase_length.rs b/crates/tek/src/tui/phrase_length.rs index aa862aa1..e6d0129f 100644 --- a/crates/tek/src/tui/phrase_length.rs +++ b/crates/tek/src/tui/phrase_length.rs @@ -2,7 +2,6 @@ use crate::*; use super::phrase_list::{PhraseListModel, PhraseListMode}; use PhraseLengthFocus::*; use PhraseLengthCommand::*; - /// Displays and edits phrase length. #[derive(Clone)] pub struct PhraseLength { @@ -15,7 +14,6 @@ pub struct PhraseLength { /// Selected subdivision pub focus: Option, } - impl PhraseLength { pub fn new (pulses: usize, focus: Option) -> Self { Self { ppq: PPQ, bpb: 4, pulses, focus } @@ -39,7 +37,6 @@ impl PhraseLength { format!("{:>02}", self.ticks()) } } - /// Focused field of `PhraseLength` #[derive(Copy, Clone, Debug)] pub enum PhraseLengthFocus { @@ -50,7 +47,6 @@ pub enum PhraseLengthFocus { /// Editing the number of ticks Tick, } - impl PhraseLengthFocus { pub fn next (&mut self) { *self = match self { @@ -67,7 +63,6 @@ impl PhraseLengthFocus { } } } - render!(|self: PhraseLength|{ let bars = ||self.bars_string(); let beats = ||self.beats_string(); @@ -83,7 +78,6 @@ render!(|self: PhraseLength|{ add(&row!([" ", bars(), ".", beats(), "[", ticks()])), }) }); - #[derive(Copy, Clone, Debug, PartialEq)] pub enum PhraseLengthCommand { Begin, @@ -94,36 +88,48 @@ pub enum PhraseLengthCommand { Inc, Dec, } - -impl Command for PhraseLengthCommand { - fn execute (self, state: &mut PhraseListModel) -> Perhaps { - match state.phrases_mode_mut().clone() { - Some(PhraseListMode::Length(phrase, ref mut length, ref mut focus)) => match self { - Cancel => { *state.phrases_mode_mut() = None; }, - Prev => { focus.prev() }, - Next => { focus.next() }, - Inc => match focus { - Bar => { *length += 4 * PPQ }, - Beat => { *length += PPQ }, - Tick => { *length += 1 }, - }, - Dec => match focus { - Bar => { *length = length.saturating_sub(4 * PPQ) }, - Beat => { *length = length.saturating_sub(PPQ) }, - Tick => { *length = length.saturating_sub(1) }, - }, - Set(length) => { - let mut phrase = state.phrases()[phrase].write().unwrap(); - let old_length = phrase.length; - phrase.length = length; - std::mem::drop(phrase); - *state.phrases_mode_mut() = None; - return Ok(Some(Self::Set(old_length))) - }, - _ => unreachable!() +command!(|self:PhraseLengthCommand,state:PhraseListModel|{ + match state.phrases_mode_mut().clone() { + Some(PhraseListMode::Length(phrase, ref mut length, ref mut focus)) => match self { + Cancel => { *state.phrases_mode_mut() = None; }, + Prev => { focus.prev() }, + Next => { focus.next() }, + Inc => match focus { + Bar => { *length += 4 * PPQ }, + Beat => { *length += PPQ }, + Tick => { *length += 1 }, + }, + Dec => match focus { + Bar => { *length = length.saturating_sub(4 * PPQ) }, + Beat => { *length = length.saturating_sub(PPQ) }, + Tick => { *length = length.saturating_sub(1) }, + }, + Set(length) => { + let mut phrase = state.phrases()[phrase].write().unwrap(); + let old_length = phrase.length; + phrase.length = length; + std::mem::drop(phrase); + *state.phrases_mode_mut() = None; + return Ok(Some(Self::Set(old_length))) }, _ => unreachable!() - }; - Ok(None) + }, + _ => unreachable!() + }; + None +}); +input_to_command!(PhraseLengthCommand:|state:PhraseListModel,from|{ + if let Some(PhraseListMode::Length(_, length, _)) = state.phrases_mode() { + match from.event() { + key_pat!(Up) => Self::Inc, + key_pat!(Down) => Self::Dec, + key_pat!(Right) => Self::Next, + key_pat!(Left) => Self::Prev, + key_pat!(Enter) => Self::Set(*length), + key_pat!(Esc) => Self::Cancel, + _ => return None + } + } else { + unreachable!() } -} +}); diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 8130336b..a7e38ad2 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -156,7 +156,6 @@ fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option return None }) } - impl Default for PhraseListModel { fn default () -> Self { Self { @@ -168,7 +167,6 @@ impl Default for PhraseListModel { } } } - impl From<&Arc>> for PhraseListModel { fn from (phrase: &Arc>) -> Self { let mut model = Self::default(); @@ -177,10 +175,8 @@ impl From<&Arc>> for PhraseListModel { model } } - has_phrases!(|self:PhraseListModel|self.phrases); has_phrase!(|self:PhraseListModel|self.phrases[self.phrase_index()]); - impl PhraseListModel { pub(crate) fn phrase_index (&self) -> usize { self.phrase.load(Relaxed) @@ -195,16 +191,7 @@ impl PhraseListModel { &mut self.mode } } - -pub trait HasPhraseList: HasPhrases { - fn phrases_focused (&self) -> bool; - fn phrases_entered (&self) -> bool; - fn phrases_mode (&self) -> &Option; - fn phrase_index (&self) -> usize; -} - pub struct PhraseListView<'a>(pub(crate) &'a PhraseListModel); - // TODO: Display phrases always in order of appearance render!(|self: PhraseListView<'a>|{ let PhraseListModel { phrases, mode, .. } = self.0; @@ -254,14 +241,12 @@ render!(|self: PhraseListView<'a>|{ add(&self.0.size) })) }); - pub struct PhraseSelector { pub(crate) title: &'static str, pub(crate) name: String, pub(crate) color: ItemPalette, pub(crate) time: String, } - // TODO: Display phrases always in order of appearance render!(|self: PhraseSelector|Fixed::wh(24, 1, row!([ Tui::fg(self.color.lightest.rgb, Tui::bold(true, &self.title)), @@ -270,7 +255,6 @@ render!(|self: PhraseSelector|Fixed::wh(24, 1, row!([ Tui::bg(self.color.dark.rgb, &self.time), ])), ]))); - impl PhraseSelector { // beats elapsed pub fn play_phrase (state: &T) -> Self { @@ -314,13 +298,69 @@ impl PhraseSelector { }; Self { title: " Next|", time, name, color, } } - pub fn edit_phrase (phrase: &Option>>) -> Self { - let (time, name, color) = if let Some(phrase) = phrase { - let phrase = phrase.read().unwrap(); - (format!("{}", phrase.length), phrase.name.clone(), phrase.color) - } else { - ("".to_string(), " ".to_string(), ItemPalette::from(TuiTheme::g(64))) - }; - Self { title: "Editing:", time, name, color } - } } +command!(|self: FileBrowserCommand, state: PhraseListModel|{ + use PhraseListMode::*; + use FileBrowserCommand::*; + let mode = &mut state.mode; + match mode { + Some(Import(index, ref mut browser)) => match self { + Cancel => { *mode = None; }, + Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); }, + Select(index) => { browser.index = index; }, + Confirm => if browser.is_file() { + let index = *index; + let path = browser.path(); + *mode = None; + PhrasePoolCommand::Import(index, path).execute(state)?; + } else if browser.is_dir() { + *mode = Some(Import(*index, browser.chdir()?)); + }, + _ => todo!(), + }, + Some(Export(index, ref mut browser)) => match self { + Cancel => { *mode = None; }, + Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); }, + Select(index) => { browser.index = index; }, + _ => unreachable!() + }, + _ => unreachable!(), + }; + None +}); +input_to_command!(FileBrowserCommand:|state:PhraseListModel,from|{ + use PhraseListMode::*; + use FileBrowserCommand::*; + use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char}; + if let Some(PhraseListMode::Import(_index, browser)) = &state.mode { + match from.event() { + key_pat!(Up) => Select(browser.index.overflowing_sub(1).0 + .min(browser.len().saturating_sub(1))), + key_pat!(Down) => Select(browser.index.saturating_add(1) + % browser.len()), + key_pat!(Right) => Chdir(browser.cwd.clone()), + key_pat!(Left) => Chdir(browser.cwd.clone()), + key_pat!(Enter) => Confirm, + key_pat!(Char(_)) => { todo!() }, + key_pat!(Backspace) => { todo!() }, + key_pat!(Esc) => Cancel, + _ => return None + } + } else if let Some(PhraseListMode::Export(_index, browser)) = &state.mode { + match from.event() { + key_pat!(Up) => Select(browser.index.overflowing_sub(1).0 + .min(browser.len())), + key_pat!(Down) => Select(browser.index.saturating_add(1) + % browser.len()), + key_pat!(Right) => Chdir(browser.cwd.clone()), + key_pat!(Left) => Chdir(browser.cwd.clone()), + key_pat!(Enter) => Confirm, + key_pat!(Char(_)) => { todo!() }, + key_pat!(Backspace) => { todo!() }, + key_pat!(Esc) => Cancel, + _ => return None + } + } else { + unreachable!() + } +}); From 417b097c6fd0c6fd5d2c0f9834a4499a96cbe115 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 13:48:52 +0100 Subject: [PATCH 020/905] apply command! in arranger (8456l) --- crates/tek/src/tui/app_arranger.rs | 57 +++++++++++------------------- 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 06e725ba..67c0fc84 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -124,44 +124,27 @@ audio!(|self: ArrangerTui, client, scope|{ Phrases(PhrasesCommand), Editor(PhraseCommand), } -impl Command for ArrangerCommand { - fn execute (self, state: &mut ArrangerTui) -> Perhaps { - use ArrangerCommand::*; - Ok(match self { - Focus(cmd) => cmd.execute(state)?.map(Focus), - Scene(cmd) => cmd.execute(state)?.map(Scene), - Track(cmd) => cmd.execute(state)?.map(Track), - Clip(cmd) => cmd.execute(state)?.map(Clip), - Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases), - Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor), - Clock(cmd) => cmd.execute(state)?.map(Clock), - Zoom(_) => { todo!(); }, - Select(selected) => { - *state.selected_mut() = selected; - None - }, - _ => { todo!() } - }) +command!(|self:ArrangerCommand,state:ArrangerTui|{ + use ArrangerCommand::*; + match self { + Focus(cmd) => cmd.execute(state)?.map(Focus), + Scene(cmd) => cmd.execute(state)?.map(Scene), + Track(cmd) => cmd.execute(state)?.map(Track), + Clip(cmd) => cmd.execute(state)?.map(Clip), + Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases), + Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor), + Clock(cmd) => cmd.execute(state)?.map(Clock), + Zoom(_) => { todo!(); }, + Select(selected) => { + *state.selected_mut() = selected; + None + }, + _ => { todo!() } } -} -impl Command for ArrangerSceneCommand { - fn execute (self, _state: &mut ArrangerTui) -> Perhaps { - //todo!(); - Ok(None) - } -} -impl Command for ArrangerTrackCommand { - fn execute (self, _state: &mut ArrangerTui) -> Perhaps { - //todo!(); - Ok(None) - } -} -impl Command for ArrangerClipCommand { - fn execute (self, _state: &mut ArrangerTui) -> Perhaps { - //todo!(); - Ok(None) - } -} +}); +command!(|self:ArrangerSceneCommand,_state:ArrangerTui|None); +command!(|self:ArrangerTrackCommand,_state:ArrangerTui|None); +command!(|self:ArrangerClipCommand, _state:ArrangerTui|None); pub trait ArrangerControl: TransportControl { fn selected (&self) -> ArrangerSelection; fn selected_mut (&mut self) -> &mut ArrangerSelection; From 1261b07aa2aae284b0323a0cca83c4170366a5d4 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 15:50:27 +0100 Subject: [PATCH 021/905] refactor some of the larger modules --- crates/tek/src/api.rs | 3 - crates/tek/src/api/note.rs | 110 ------ crates/tek/src/api/player.rs | 523 ----------------------------- crates/tek/src/core.rs | 1 - crates/tek/src/core/audio.rs | 181 ---------- crates/tek/src/core/collect.rs | 0 crates/tek/src/core/perf.rs | 2 +- crates/tek/src/{api => }/jack.rs | 226 +------------ crates/tek/src/jack/activate.rs | 50 +++ crates/tek/src/jack/audio.rs | 78 +++++ crates/tek/src/jack/client.rs | 61 ++++ crates/tek/src/jack/from_jack.rs | 13 + crates/tek/src/jack/jack_event.rs | 69 ++++ crates/tek/src/jack/ports.rs | 39 +++ crates/tek/src/lib.rs | 34 +- crates/tek/src/midi.rs | 36 ++ crates/tek/src/midi/midi_in.rs | 10 + crates/tek/src/midi/midi_launch.rs | 35 ++ crates/tek/src/midi/midi_note.rs | 110 ++++++ crates/tek/src/midi/midi_out.rs | 12 + crates/tek/src/midi/midi_play.rs | 134 ++++++++ crates/tek/src/midi/midi_player.rs | 255 ++++++++++++++ crates/tek/src/midi/midi_rec.rs | 75 +++++ crates/tek/src/tui/app_arranger.rs | 63 ++++ 24 files changed, 1070 insertions(+), 1050 deletions(-) delete mode 100644 crates/tek/src/api/player.rs delete mode 100644 crates/tek/src/core/audio.rs delete mode 100644 crates/tek/src/core/collect.rs rename crates/tek/src/{api => }/jack.rs (54%) create mode 100644 crates/tek/src/jack/activate.rs create mode 100644 crates/tek/src/jack/audio.rs create mode 100644 crates/tek/src/jack/client.rs create mode 100644 crates/tek/src/jack/from_jack.rs create mode 100644 crates/tek/src/jack/jack_event.rs create mode 100644 crates/tek/src/jack/ports.rs create mode 100644 crates/tek/src/midi.rs create mode 100644 crates/tek/src/midi/midi_in.rs create mode 100644 crates/tek/src/midi/midi_launch.rs create mode 100644 crates/tek/src/midi/midi_note.rs create mode 100644 crates/tek/src/midi/midi_out.rs create mode 100644 crates/tek/src/midi/midi_play.rs create mode 100644 crates/tek/src/midi/midi_player.rs create mode 100644 crates/tek/src/midi/midi_rec.rs diff --git a/crates/tek/src/api.rs b/crates/tek/src/api.rs index 96cc6722..4a83fd07 100644 --- a/crates/tek/src/api.rs +++ b/crates/tek/src/api.rs @@ -1,8 +1,5 @@ -mod jack; pub(crate) use self::jack::*; mod phrase; pub(crate) use phrase::*; mod clock; pub(crate) use clock::*; -mod note; pub(crate) use note::*; -mod player; pub(crate) use player::*; mod scene; pub(crate) use scene::*; mod track; pub(crate) use track::*; mod sampler; pub(crate) use sampler::*; diff --git a/crates/tek/src/api/note.rs b/crates/tek/src/api/note.rs index abbc4bc0..e69de29b 100644 --- a/crates/tek/src/api/note.rs +++ b/crates/tek/src/api/note.rs @@ -1,110 +0,0 @@ -use crate::*; -use Ordering::Relaxed; - -pub trait MidiViewport: MidiRange + MidiPoint + HasSize { - /// Make sure cursor is within range - fn autoscroll (&self) { - let note_lo = self.note_lo(); - let note_axis = self.note_axis(); - let note_hi = self.note_hi(); - let note_point = self.note_point().min(127); - if note_point < note_lo { - self.set_note_lo(note_point); - } else if note_point > note_hi { - self.set_note_lo((note_lo + note_point).saturating_sub(note_hi)); - } - } - /// Make sure best usage of screen space is achieved by default - fn autozoom (&self) { - } -} - -#[derive(Debug, Clone)] -pub struct MidiRangeModel { - /// Length of visible time axis - pub time_axis: Arc, - /// Earliest time displayed - pub time_start: Arc, - /// Time step - pub time_zoom: Arc, - /// Auto rezoom to fit in time axis - pub time_lock: Arc, - /// Length of visible note axis - pub note_axis: Arc, - // Lowest note displayed - pub note_lo: Arc, -} -impl From<(usize, bool)> for MidiRangeModel { - fn from ((time_zoom, time_lock): (usize, bool)) -> Self { - Self { - note_axis: Arc::new(0.into()), - note_lo: Arc::new(0.into()), - time_axis: Arc::new(0.into()), - time_start: Arc::new(0.into()), - time_zoom: Arc::new(time_zoom.into()), - time_lock: Arc::new(time_lock.into()), - } - } -} -pub trait MidiRange { - fn time_zoom (&self) -> usize; - fn set_time_zoom (&mut self, x: usize); - fn time_lock (&self) -> bool; - fn set_time_lock (&self, x: bool); - fn time_start (&self) -> usize; - fn set_time_start (&self, x: usize); - fn note_lo (&self) -> usize; - fn set_note_lo (&self, x: usize); - fn note_axis (&self) -> usize; - fn time_axis (&self) -> usize; - fn note_hi (&self) -> usize { (self.note_lo() + self.note_axis().saturating_sub(1)).min(127) } - fn time_end (&self) -> usize { self.time_start() + self.time_axis() * self.time_zoom() } -} -impl MidiRange for MidiRangeModel { - fn time_zoom (&self) -> usize { self.time_zoom.load(Relaxed) } - fn set_time_zoom (&mut self, x: usize) { self.time_zoom.store(x, Relaxed); } - fn time_lock (&self) -> bool { self.time_lock.load(Relaxed) } - fn set_time_lock (&self, x: bool) { self.time_lock.store(x, Relaxed); } - fn time_start (&self) -> usize { self.time_start.load(Relaxed) } - fn set_time_start (&self, x: usize) { self.time_start.store(x, Relaxed); } - fn note_lo (&self) -> usize { self.note_lo.load(Relaxed).min(127) } - fn set_note_lo (&self, x: usize) { self.note_lo.store(x.min(127), Relaxed); } - fn note_axis (&self) -> usize { self.note_axis.load(Relaxed) } - fn time_axis (&self) -> usize { self.time_axis.load(Relaxed) } -} - -#[derive(Debug, Clone)] -pub struct MidiPointModel { - /// Time coordinate of cursor - pub time_point: Arc, - /// Note coordinate of cursor - pub note_point: Arc, - /// Length of note that will be inserted, in pulses - pub note_len: Arc, -} -impl Default for MidiPointModel { - fn default () -> Self { - Self { - time_point: Arc::new(0.into()), - note_point: Arc::new(36.into()), - note_len: Arc::new(24.into()), - } - } -} -pub trait MidiPoint { - fn note_len (&self) -> usize; - fn set_note_len (&self, x: usize); - fn note_point (&self) -> usize; - fn set_note_point (&self, x: usize); - fn time_point (&self) -> usize; - fn set_time_point (&self, x: usize); - fn note_end (&self) -> usize { self.note_point() + self.note_len() } -} -impl MidiPoint for MidiPointModel { - fn note_len (&self) -> usize { self.note_len.load(Relaxed)} - fn set_note_len (&self, x: usize) { self.note_len.store(x, Relaxed) } - fn note_point (&self) -> usize { self.note_point.load(Relaxed).min(127) } - fn set_note_point (&self, x: usize) { self.note_point.store(x.min(127), Relaxed) } - fn time_point (&self) -> usize { self.time_point.load(Relaxed) } - fn set_time_point (&self, x: usize) { self.time_point.store(x, Relaxed) } -} diff --git a/crates/tek/src/api/player.rs b/crates/tek/src/api/player.rs deleted file mode 100644 index ff11e44f..00000000 --- a/crates/tek/src/api/player.rs +++ /dev/null @@ -1,523 +0,0 @@ -use crate::*; - -pub trait HasPlayer { - fn player (&self) -> &impl MidiPlayerApi; - fn player_mut (&mut self) -> &mut impl MidiPlayerApi; -} - -#[macro_export] macro_rules! has_player { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? { - fn player (&$self) -> &impl MidiPlayerApi { &$cb } - fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb } - } - } -} - -/// Contains state for playing a phrase -pub struct PhrasePlayerModel { - /// State of clock and playhead - pub(crate) clock: ClockModel, - /// Start time and phrase being played - pub(crate) play_phrase: Option<(Moment, Option>>)>, - /// Start time and next phrase - pub(crate) next_phrase: Option<(Moment, Option>>)>, - /// Play input through output. - pub(crate) monitoring: bool, - /// Write input to sequence. - pub(crate) recording: bool, - /// Overdub input to sequence. - pub(crate) overdub: bool, - /// Send all notes off - pub(crate) reset: bool, // TODO?: after Some(nframes) - /// Record from MIDI ports to current sequence. - pub midi_ins: Vec>, - /// Play from current sequence to MIDI ports - pub midi_outs: Vec>, - /// Notes currently held at input - pub(crate) notes_in: Arc>, - /// Notes currently held at output - pub(crate) notes_out: Arc>, - /// MIDI output buffer - pub note_buf: Vec, -} - -impl std::fmt::Debug for PhrasePlayerModel { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("PhrasePlayerModel") - .field("clock", &self.clock) - .field("play_phrase", &self.play_phrase) - .field("next_phrase", &self.next_phrase) - .finish() - } -} - -impl From<&ClockModel> for PhrasePlayerModel { - fn from (clock: &ClockModel) -> Self { - Self { - clock: clock.clone(), - midi_ins: vec![], - midi_outs: vec![], - note_buf: vec![0;8], - reset: true, - recording: false, - monitoring: false, - overdub: false, - play_phrase: None, - next_phrase: None, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - } - } -} - -impl From<(&ClockModel, &Arc>)> for PhrasePlayerModel { - fn from ((clock, phrase): (&ClockModel, &Arc>)) -> Self { - let mut model = Self::from(clock); - model.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone()))); - model - } -} - -has_clock!(|self:PhrasePlayerModel|&self.clock); - -impl HasMidiIns for PhrasePlayerModel { - fn midi_ins (&self) -> &Vec> { - &self.midi_ins - } - fn midi_ins_mut (&mut self) -> &mut Vec> { - &mut self.midi_ins - } -} - -impl HasMidiOuts for PhrasePlayerModel { - fn midi_outs (&self) -> &Vec> { - &self.midi_outs - } - fn midi_outs_mut (&mut self) -> &mut Vec> { - &mut self.midi_outs - } - fn midi_note (&mut self) -> &mut Vec { - &mut self.note_buf - } -} - -pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {} - -impl MidiPlayerApi for PhrasePlayerModel {} - -pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns { - fn notes_in (&self) -> &Arc>; - - fn recording (&self) -> bool; - fn recording_mut (&mut self) -> &mut bool; - fn toggle_record (&mut self) { - *self.recording_mut() = !self.recording(); - } - fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { - let sample0 = scope.last_frame_time() as usize; - // For highlighting keys and note repeat - let notes_in = self.notes_in().clone(); - if self.clock().is_rolling() { - if let Some((started, ref phrase)) = self.play_phrase().clone() { - let start = started.sample.get() as usize; - let quant = self.clock().quant.get(); - let timebase = self.clock().timebase().clone(); - let monitoring = self.monitoring(); - let recording = self.recording(); - for input in self.midi_ins_mut().iter() { - for (sample, event, bytes) in parse_midi_input(input.iter(scope)) { - if let LiveEvent::Midi { message, .. } = event { - if monitoring { - midi_buf[sample].push(bytes.to_vec()) - } - if recording { - if let Some(phrase) = phrase { - let mut phrase = phrase.write().unwrap(); - let length = phrase.length; - phrase.record_event({ - let sample = (sample0 + sample - start) as f64; - let pulse = timebase.samples_to_pulse(sample); - let quantized = (pulse / quant).round() * quant; - let looped = quantized as usize % length; - looped - }, message); - } - } - update_keys(&mut*notes_in.write().unwrap(), &message); - } - } - } - } - if let Some((start_at, phrase)) = &self.next_phrase() { - // TODO switch to next phrase and record into it - } - } - } - - fn monitoring (&self) -> bool; - fn monitoring_mut (&mut self) -> &mut bool; - fn toggle_monitor (&mut self) { - *self.monitoring_mut() = !self.monitoring(); - } - fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { - // For highlighting keys and note repeat - let notes_in = self.notes_in().clone(); - for input in self.midi_ins_mut().iter() { - for (sample, event, bytes) in parse_midi_input(input.iter(scope)) { - if let LiveEvent::Midi { message, .. } = event { - midi_buf[sample].push(bytes.to_vec()); - update_keys(&mut*notes_in.write().unwrap(), &message); - } - } - } - } - - fn overdub (&self) -> bool; - fn overdub_mut (&mut self) -> &mut bool; - fn toggle_overdub (&mut self) { - *self.overdub_mut() = !self.overdub(); - } -} - -impl MidiRecordApi for PhrasePlayerModel { - fn recording (&self) -> bool { - self.recording - } - fn recording_mut (&mut self) -> &mut bool { - &mut self.recording - } - fn monitoring (&self) -> bool { - self.monitoring - } - fn monitoring_mut (&mut self) -> &mut bool { - &mut self.monitoring - } - fn overdub (&self) -> bool { - self.overdub - } - fn overdub_mut (&mut self) -> &mut bool { - &mut self.overdub - } - fn notes_in (&self) -> &Arc> { - &self.notes_in - } -} - -pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts { - - fn notes_out (&self) -> &Arc>; - - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - fn clear ( - &mut self, scope: &ProcessScope, out_buf: &mut Vec>>, reset: bool - ) { - for frame in &mut out_buf[0..scope.n_frames() as usize] { - frame.clear(); - } - if reset { - all_notes_off(out_buf); - } - } - - /// Output notes from phrase to MIDI output ports. - fn play ( - &mut self, scope: &ProcessScope, note_buf: &mut Vec, out_buf: &mut Vec>> - ) -> bool { - let mut next = false; - // Write MIDI events from currently playing phrase (if any) to MIDI output buffer - if self.clock().is_rolling() { - let sample0 = scope.last_frame_time() as usize; - let samples = scope.n_frames() as usize; - // If no phrase is playing, prepare for switchover immediately - next = self.play_phrase().is_none(); - let phrase = self.play_phrase(); - let started0 = &self.clock().started; - let timebase = self.clock().timebase(); - let notes_out = self.notes_out(); - let next_phrase = self.next_phrase(); - if let Some((started, phrase)) = phrase { - // First sample to populate. Greater than 0 means that the first - // pulse of the phrase falls somewhere in the middle of the chunk. - let sample = started.sample.get() as usize; - let sample = sample + started0.read().unwrap().as_ref().unwrap().sample.get() as usize; - let sample = sample0.saturating_sub(sample); - // Iterator that emits sample (index into output buffer at which to write MIDI event) - // paired with pulse (index into phrase from which to take the MIDI event) for each - // sample of the output buffer that corresponds to a MIDI pulse. - let pulses = timebase.pulses_between_samples(sample, sample + samples); - // Notes active during current chunk. - let notes = &mut notes_out.write().unwrap(); - for (sample, pulse) in pulses { - // If a next phrase is enqueued, and we're past the end of the current one, - // break the loop here (FIXME count pulse correctly) - next = next_phrase.is_some() && if let Some(ref phrase) = phrase { - pulse >= phrase.read().unwrap().length - } else { - true - }; - if next { - break - } - // If there's a currently playing phrase, output notes from it to buffer: - if let Some(ref phrase) = phrase { - // Source phrase from which the MIDI events will be taken. - let phrase = phrase.read().unwrap(); - // Phrase with zero length is not processed - if phrase.length > 0 { - // Current pulse index in source phrase - let pulse = pulse % phrase.length; - // Output each MIDI event from phrase at appropriate frames of output buffer: - for message in phrase.notes[pulse].iter() { - // Clear output buffer for this MIDI event. - note_buf.clear(); - // TODO: support MIDI channels other than CH1. - let channel = 0.into(); - // Serialize MIDI event into message buffer. - LiveEvent::Midi { channel, message: *message } - .write(note_buf) - .unwrap(); - // Append serialized message to output buffer. - out_buf[sample].push(note_buf.clone()); - // Update the list of currently held notes. - update_keys(&mut*notes, &message); - } - } - } - } - } - } - next - } - - /// Handle switchover from current to next playing phrase. - fn switchover ( - &mut self, scope: &ProcessScope, note_buf: &mut Vec, out_buf: &mut Vec>> - ) { - if self.clock().is_rolling() { - let sample0 = scope.last_frame_time() as usize; - //let samples = scope.n_frames() as usize; - if let Some((start_at, phrase)) = &self.next_phrase() { - let start = start_at.sample.get() as usize; - let sample = self.clock().started.read().unwrap().as_ref().unwrap().sample.get() as usize; - // If it's time to switch to the next phrase: - if start <= sample0.saturating_sub(sample) { - // Samples elapsed since phrase was supposed to start - let skipped = sample0 - start; - // Switch over to enqueued phrase - let started = Moment::from_sample(&self.clock().timebase(), start as f64); - *self.play_phrase_mut() = Some((started, phrase.clone())); - // Unset enqueuement (TODO: where to implement looping?) - *self.next_phrase_mut() = None - } - // TODO fill in remaining ticks of chunk from next phrase. - // ?? just call self.play(scope) again, since enqueuement is off ??? - self.play(scope, note_buf, out_buf); - // ?? or must it be with modified scope ?? - // likely not because start time etc - } - } - } - - /// Write a chunk of MIDI notes to the output buffer. - fn write ( - &mut self, scope: &ProcessScope, out_buf: &Vec>> - ) { - let samples = scope.n_frames() as usize; - for port in self.midi_outs_mut().iter_mut() { - let writer = &mut port.writer(scope); - for time in 0..samples { - for event in out_buf[time].iter() { - writer.write(&RawMidi { time: time as u32, bytes: &event }) - .expect(&format!("{event:?}")); - } - } - } - } -} - -impl MidiPlaybackApi for PhrasePlayerModel { - fn notes_out (&self) -> &Arc> { - &self.notes_in - } -} - -pub trait HasPlayPhrase: HasClock { - fn reset (&self) -> bool; - fn reset_mut (&mut self) -> &mut bool; - fn play_phrase (&self) -> &Option<(Moment, Option>>)>; - fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)>; - fn next_phrase (&self) -> &Option<(Moment, Option>>)>; - fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)>; - fn pulses_since_start (&self) -> Option { - if let Some((started, Some(_))) = self.play_phrase().as_ref() { - let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); - Some(elapsed) - } else { - None - } - } - fn pulses_since_start_looped (&self) -> Option { - if let Some((started, Some(phrase))) = self.play_phrase().as_ref() { - let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); - let length = phrase.read().unwrap().length.max(1); // prevent div0 on empty phrase - let elapsed = (elapsed as usize % length) as f64; - Some(elapsed) - } else { - None - } - } - fn enqueue_next (&mut self, phrase: Option<&Arc>>) { - let start = self.clock().next_launch_pulse() as f64; - let instant = Moment::from_pulse(&self.clock().timebase(), start); - let phrase = phrase.map(|p|p.clone()); - *self.next_phrase_mut() = Some((instant, phrase)); - *self.reset_mut() = true; - } -} - -impl HasPlayPhrase for PhrasePlayerModel { - fn reset (&self) -> bool { - self.reset - } - fn reset_mut (&mut self) -> &mut bool { - &mut self.reset - } - fn play_phrase (&self) -> &Option<(Moment, Option>>)> { - &self.play_phrase - } - fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.play_phrase - } - fn next_phrase (&self) -> &Option<(Moment, Option>>)> { - &self.next_phrase - } - fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.next_phrase - } -} - -/// Add "all notes off" to the start of a buffer. -pub fn all_notes_off (output: &mut [Vec>]) { - let mut buf = vec![]; - let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; - let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; - 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 }|( - time as usize, - LiveEvent::parse(bytes).unwrap(), - bytes - ))) -} - -/// Update notes_in array -pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { - match message { - MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } - MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, - _ => {} - } -} - -/// Hosts the JACK callback for a single MIDI player -pub struct PlayerAudio<'a, T: MidiPlayerApi>( - /// Player - pub &'a mut T, - /// Note buffer - pub &'a mut Vec, - /// Note chunk buffer - pub &'a mut Vec>>, -); - -/// JACK process callback for a sequencer's phrase player/recorder. -impl<'a, T: MidiPlayerApi> Audio for PlayerAudio<'a, T> { - fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - let model = &mut self.0; - let note_buf = &mut self.1; - let midi_buf = &mut self.2; - // Clear output buffer(s) - model.clear(scope, midi_buf, false); - // Write chunk of phrase to output, handle switchover - if model.play(scope, note_buf, midi_buf) { - model.switchover(scope, note_buf, midi_buf); - } - if model.has_midi_ins() { - if model.recording() || model.monitoring() { - // Record and/or monitor input - model.record(scope, midi_buf) - } else if model.has_midi_outs() && model.monitoring() { - // Monitor input to output - model.monitor(scope, midi_buf) - } - } - // Write to output port(s) - model.write(scope, midi_buf); - Control::Continue - } -} - -//#[derive(Debug)] -//pub struct MIDIPlayer { - ///// Global timebase - //pub clock: Arc, - ///// Start time and phrase being played - //pub play_phrase: Option<(Moment, Option>>)>, - ///// Start time and next phrase - //pub next_phrase: Option<(Moment, Option>>)>, - ///// Play input through output. - //pub monitoring: bool, - ///// Write input to sequence. - //pub recording: bool, - ///// Overdub input to sequence. - //pub overdub: bool, - ///// Send all notes off - //pub reset: bool, // TODO?: after Some(nframes) - ///// Record from MIDI ports to current sequence. - //pub midi_inputs: Vec>, - ///// Play from current sequence to MIDI ports - //pub midi_outputs: Vec>, - ///// MIDI output buffer - //pub midi_note: Vec, - ///// MIDI output buffer - //pub midi_chunk: Vec>>, - ///// Notes currently held at input - //pub notes_in: Arc>, - ///// Notes currently held at output - //pub notes_out: Arc>, -//} - -///// Methods used primarily by the process callback -//impl MIDIPlayer { - //pub fn new ( - //jack: &Arc>, - //clock: &Arc, - //name: &str - //) -> Usually { - //let jack = jack.read().unwrap(); - //Ok(Self { - //clock: clock.clone(), - //phrase: None, - //next_phrase: None, - //notes_in: Arc::new(RwLock::new([false;128])), - //notes_out: Arc::new(RwLock::new([false;128])), - //monitoring: false, - //recording: false, - //overdub: true, - //reset: true, - //midi_note: Vec::with_capacity(8), - //midi_chunk: vec![Vec::with_capacity(16);16384], - //midi_outputs: vec![ - //jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())? - //], - //midi_inputs: vec![ - //jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())? - //], - //}) - //} -//} diff --git a/crates/tek/src/core.rs b/crates/tek/src/core.rs index caacfa57..a7cb629a 100644 --- a/crates/tek/src/core.rs +++ b/crates/tek/src/core.rs @@ -1,4 +1,3 @@ -pub(crate) mod audio; pub(crate) use audio::*; pub(crate) mod color; pub(crate) use color::*; pub(crate) mod command; pub(crate) use command::*; pub(crate) mod engine; pub(crate) use engine::*; diff --git a/crates/tek/src/core/audio.rs b/crates/tek/src/core/audio.rs deleted file mode 100644 index e9a50577..00000000 --- a/crates/tek/src/core/audio.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::*; -use jack::*; - -#[derive(Debug, Clone)] -/// Event enum for JACK events. -pub enum JackEvent { - ThreadInit, - Shutdown(ClientStatus, String), - Freewheel(bool), - SampleRate(Frames), - ClientRegistration(String, bool), - PortRegistration(PortId, bool), - PortRename(PortId, String, String), - PortsConnected(PortId, PortId, bool), - GraphReorder, - XRun, -} - -/// Wraps [Client] or [DynamicAsyncClient] in place. -#[derive(Debug)] -pub enum JackClient { - /// Before activation. - Inactive(Client), - /// During activation. - Activating, - /// After activation. Must not be dropped for JACK thread to persist. - Active(DynamicAsyncClient), -} - -/// Trait for things that wrap a JACK client. -pub trait AudioEngine { - - fn transport (&self) -> Transport { - self.client().transport() - } - - fn port_by_name (&self, name: &str) -> Option> { - self.client().port_by_name(name) - } - - fn register_port (&self, name: &str, spec: PS) -> Usually> { - Ok(self.client().register_port(name, spec)?) - } - - fn client (&self) -> &Client; - - fn activate ( - self, - process: impl FnMut(&Arc>, &Client, &ProcessScope) -> Control + Send + 'static - ) -> Usually>> where Self: Send + Sync + 'static; - - fn thread_init (&self, _: &Client) {} - - unsafe fn shutdown (&mut self, _status: ClientStatus, _reason: &str) {} - - fn freewheel (&mut self, _: &Client, _enabled: bool) {} - - fn client_registration (&mut self, _: &Client, _name: &str, _reg: bool) {} - - fn port_registration (&mut self, _: &Client, _id: PortId, _reg: bool) {} - - fn ports_connected (&mut self, _: &Client, _a: PortId, _b: PortId, _are: bool) {} - - fn sample_rate (&mut self, _: &Client, _frames: Frames) -> Control { - Control::Continue - } - - fn port_rename (&mut self, _: &Client, _id: PortId, _old: &str, _new: &str) -> Control { - Control::Continue - } - - fn graph_reorder (&mut self, _: &Client) -> Control { - Control::Continue - } - - fn xrun (&mut self, _: &Client) -> Control { - Control::Continue - } -} - -impl AudioEngine for JackClient { - fn client(&self) -> &Client { - match self { - Self::Inactive(ref client) => client, - Self::Activating => panic!("jack client has not finished activation"), - Self::Active(ref client) => client.as_client(), - } - } - fn activate( - self, - mut cb: impl FnMut(&Arc>, &Client, &ProcessScope) -> Control + Send + 'static, - ) -> Usually>> - where - Self: Send + Sync + 'static - { - let client = Client::from(self); - let state = Arc::new(RwLock::new(Self::Activating)); - let event = Box::new(move|_|{/*TODO*/}) as Box; - let events = Notifications(event); - let frame = Box::new({let state = state.clone(); move|c: &_, s: &_|cb(&state, c, s)}); - let frames = contrib::ClosureProcessHandler::new(frame as BoxedAudioHandler); - *state.write().unwrap() = Self::Active(client.activate_async(events, frames)?); - Ok(state) - } -} - -pub type DynamicAsyncClient = AsyncClient; - -pub type DynamicAudioHandler = contrib::ClosureProcessHandler<(), BoxedAudioHandler>; - -pub type BoxedAudioHandler = Box Control + Send>; - -impl JackClient { - pub fn new (name: &str) -> Usually { - let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; - Ok(Self::Inactive(client)) - } -} - -impl From for Client { - fn from (jack: JackClient) -> Client { - match jack { - JackClient::Inactive(client) => client, - JackClient::Activating => panic!("jack client still activating"), - JackClient::Active(_) => panic!("jack client already activated"), - } - } -} - -/// Notification handler used by the [Jack] factory -/// when constructing [JackDevice]s. -pub type DynamicNotifications = Notifications>; - -/// Generic notification handler that emits [JackEvent] -pub struct Notifications(pub T); - -impl NotificationHandler for Notifications { - fn thread_init(&self, _: &Client) { - self.0(JackEvent::ThreadInit); - } - - unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { - self.0(JackEvent::Shutdown(status, reason.into())); - } - - fn freewheel(&mut self, _: &Client, enabled: bool) { - self.0(JackEvent::Freewheel(enabled)); - } - - fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { - self.0(JackEvent::SampleRate(frames)); - Control::Quit - } - - fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { - self.0(JackEvent::ClientRegistration(name.into(), reg)); - } - - fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { - self.0(JackEvent::PortRegistration(id, reg)); - } - - fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { - self.0(JackEvent::PortRename(id, old.into(), new.into())); - Control::Continue - } - - fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { - self.0(JackEvent::PortsConnected(a, b, are)); - } - - fn graph_reorder(&mut self, _: &Client) -> Control { - self.0(JackEvent::GraphReorder); - Control::Continue - } - - fn xrun(&mut self, _: &Client) -> Control { - self.0(JackEvent::XRun); - Control::Continue - } -} diff --git a/crates/tek/src/core/collect.rs b/crates/tek/src/core/collect.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/tek/src/core/perf.rs b/crates/tek/src/core/perf.rs index 05403b11..e389a7c3 100644 --- a/crates/tek/src/core/perf.rs +++ b/crates/tek/src/core/perf.rs @@ -29,7 +29,7 @@ impl PerfModel { None } } - pub fn update (&self, t0: Option, scope: &jack::ProcessScope) { + pub fn update (&self, t0: Option, scope: &ProcessScope) { if let Some(t0) = t0 { let t1 = self.clock.raw(); self.used.store( diff --git a/crates/tek/src/api/jack.rs b/crates/tek/src/jack.rs similarity index 54% rename from crates/tek/src/api/jack.rs rename to crates/tek/src/jack.rs index 6ce07fcd..ac04ed72 100644 --- a/crates/tek/src/api/jack.rs +++ b/crates/tek/src/jack.rs @@ -1,151 +1,14 @@ use crate::*; -/// Implement [TryFrom<&Arc>>]: create app state from wrapped JACK handle. -#[macro_export] macro_rules! from_jack { - (|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc>> for $Struct $(<$($L),*$($T),*>)? { - type Error = Box; - fn try_from ($jack: &Arc>) -> Usually { - Ok($cb) - } - } - }; -} - -/// Trait for thing that has a JACK process callback. -pub trait Audio: Send + Sync { - fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { - Control::Continue - } - fn callback ( - state: &Arc>, client: &Client, scope: &ProcessScope - ) -> Control where Self: Sized { - if let Ok(mut state) = state.write() { - state.process(client, scope) - } else { - Control::Quit - } - } -} - -/// Implement [Audio]: provide JACK callbacks. -#[macro_export] macro_rules! audio { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { - #[inline] fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control { $cb } - } - } -} - -/// Trait for thing that may receive MIDI. -pub trait HasMidiIns { - fn midi_ins (&self) -> &Vec>; - fn midi_ins_mut (&mut self) -> &mut Vec>; - fn has_midi_ins (&self) -> bool { - self.midi_ins().len() > 0 - } -} - -/// Trait for thing that may output MIDI. -pub trait HasMidiOuts { - fn midi_outs (&self) -> &Vec>; - fn midi_outs_mut (&mut self) -> &mut Vec>; - fn has_midi_outs (&self) -> bool { - self.midi_outs().len() > 0 - } - /// Buffer for serializing a MIDI event. FIXME rename - fn midi_note (&mut self) -> &mut Vec; -} +pub(crate) mod activate; pub(crate) use activate::*; +pub(crate) mod audio; pub(crate) use audio::*; +pub(crate) mod client; pub(crate) use client::*; +pub(crate) mod from_jack; pub(crate) use from_jack::*; +pub(crate) mod jack_event; pub(crate) use jack_event::*; +pub(crate) mod ports; pub(crate) use ports::*; //////////////////////////////////////////////////////////////////////////////////// -pub trait JackActivate: Sized { - fn activate_with ( - self, - init: impl FnOnce(&Arc>)->Usually - ) - -> Usually>>; -} - -impl JackActivate for JackClient { - fn activate_with ( - self, - init: impl FnOnce(&Arc>)->Usually - ) - -> Usually>> - { - let client = Arc::new(RwLock::new(self)); - let target = Arc::new(RwLock::new(init(&client)?)); - let event = Box::new(move|_|{/*TODO*/}) as Box; - let events = Notifications(event); - let frame = Box::new({ - let target = target.clone(); - move|c: &_, s: &_|if let Ok(mut target) = target.write() { - target.process(c, s) - } else { - Control::Quit - } - }); - let frames = ClosureProcessHandler::new(frame as BoxedAudioHandler); - let mut buffer = Self::Activating; - std::mem::swap(&mut*client.write().unwrap(), &mut buffer); - *client.write().unwrap() = Self::Active(Client::from(buffer).activate_async(events, frames)?); - Ok(target) - } -} - -/// A UI component that may be associated with a JACK client by the `Jack` factory. -pub trait AudioComponent: Component + Audio { - /// Perform type erasure for collecting heterogeneous devices. - fn boxed(self) -> Box> - where - Self: Sized + 'static, - { - Box::new(self) - } -} - -/// All things that implement the required traits can be treated as `AudioComponent`. -impl + Audio> AudioComponent for W {} - -/// Trait for things that may expose JACK ports. -pub trait Ports { - fn audio_ins(&self) -> Usually>> { - Ok(vec![]) - } - fn audio_outs(&self) -> Usually>> { - Ok(vec![]) - } - fn midi_ins(&self) -> Usually>> { - Ok(vec![]) - } - fn midi_outs(&self) -> Usually>> { - Ok(vec![]) - } -} - -fn register_ports( - client: &Client, - names: Vec, - spec: T, -) -> Usually>> { - names - .into_iter() - .try_fold(BTreeMap::new(), |mut ports, name| { - let port = client.register_port(&name, spec)?; - ports.insert(name, port); - Ok(ports) - }) -} - -fn query_ports(client: &Client, names: Vec) -> BTreeMap> { - names.into_iter().fold(BTreeMap::new(), |mut ports, name| { - let port = client.port_by_name(&name).unwrap(); - ports.insert(name, port); - ports - }) -} - ///// A [AudioComponent] bound to a JACK client and a set of ports. //pub struct JackDevice { ///// The active JACK client of this device. @@ -373,7 +236,7 @@ fn query_ports(client: &Client, names: Vec) -> BTreeMap), - //contrib::ClosureProcessHandler::new(Box::new({ + //ClosureProcessHandler::new(Box::new({ //let state = state.clone(); //move |c: &Client, s: &ProcessScope| state.write().unwrap().process(c, s) //}) as BoxedAudioHandler), @@ -406,78 +269,3 @@ fn query_ports(client: &Client, names: Vec) -> BTreeMap for ArrangerSceneCommand { -//} - //Edit(phrase) => { state.state.phrase = phrase.clone() }, - //ToggleViewMode => { state.state.mode.to_next(); }, - //Delete => { state.state.delete(); }, - //Activate => { state.state.activate(); }, - //ZoomIn => { state.state.zoom_in(); }, - //ZoomOut => { state.state.zoom_out(); }, - //MoveBack => { state.state.move_back(); }, - //MoveForward => { state.state.move_forward(); }, - //RandomColor => { state.state.randomize_color(); }, - //Put => { state.state.phrase_put(); }, - //Get => { state.state.phrase_get(); }, - //AddScene => { state.state.scene_add(None, None)?; }, - //AddTrack => { state.state.track_add(None, None)?; }, - //ToggleLoop => { state.state.toggle_loop() }, - //pub fn zoom_in (&mut self) { - //if let ArrangerEditorMode::Vertical(factor) = self.mode { - //self.mode = ArrangerEditorMode::Vertical(factor + 1) - //} - //} - //pub fn zoom_out (&mut self) { - //if let ArrangerEditorMode::Vertical(factor) = self.mode { - //self.mode = ArrangerEditorMode::Vertical(factor.saturating_sub(1)) - //} - //} - //pub fn move_back (&mut self) { - //match self.selected { - //ArrangerEditorFocus::Scene(s) => { - //if s > 0 { - //self.scenes.swap(s, s - 1); - //self.selected = ArrangerEditorFocus::Scene(s - 1); - //} - //}, - //ArrangerEditorFocus::Track(t) => { - //if t > 0 { - //self.tracks.swap(t, t - 1); - //self.selected = ArrangerEditorFocus::Track(t - 1); - //// FIXME: also swap clip order in scenes - //} - //}, - //_ => todo!("arrangement: move forward") - //} - //} - //pub fn move_forward (&mut self) { - //match self.selected { - //ArrangerEditorFocus::Scene(s) => { - //if s < self.scenes.len().saturating_sub(1) { - //self.scenes.swap(s, s + 1); - //self.selected = ArrangerEditorFocus::Scene(s + 1); - //} - //}, - //ArrangerEditorFocus::Track(t) => { - //if t < self.tracks.len().saturating_sub(1) { - //self.tracks.swap(t, t + 1); - //self.selected = ArrangerEditorFocus::Track(t + 1); - //// FIXME: also swap clip order in scenes - //} - //}, - //_ => todo!("arrangement: move forward") - //} - //} - -//impl From for Clock { - //fn from (current: Moment) -> Self { - //Self { - //playing: Some(TransportState::Stopped).into(), - //started: None.into(), - //quant: 24.into(), - //sync: (current.timebase.ppq.get() * 4.).into(), - //current, - //} - //} -//} diff --git a/crates/tek/src/jack/activate.rs b/crates/tek/src/jack/activate.rs new file mode 100644 index 00000000..37829f3c --- /dev/null +++ b/crates/tek/src/jack/activate.rs @@ -0,0 +1,50 @@ +use crate::*; + +pub trait JackActivate: Sized { + fn activate_with ( + self, + init: impl FnOnce(&Arc>)->Usually + ) + -> Usually>>; +} + +impl JackActivate for JackClient { + fn activate_with ( + self, + init: impl FnOnce(&Arc>)->Usually + ) + -> Usually>> + { + let client = Arc::new(RwLock::new(self)); + let target = Arc::new(RwLock::new(init(&client)?)); + let event = Box::new(move|_|{/*TODO*/}) as Box; + let events = Notifications(event); + let frame = Box::new({ + let target = target.clone(); + move|c: &_, s: &_|if let Ok(mut target) = target.write() { + target.process(c, s) + } else { + Control::Quit + } + }); + let frames = ClosureProcessHandler::new(frame as BoxedAudioHandler); + let mut buffer = Self::Activating; + std::mem::swap(&mut*client.write().unwrap(), &mut buffer); + *client.write().unwrap() = Self::Active(Client::from(buffer).activate_async(events, frames)?); + Ok(target) + } +} + +/// A UI component that may be associated with a JACK client by the `Jack` factory. +pub trait AudioComponent: Component + Audio { + /// Perform type erasure for collecting heterogeneous devices. + fn boxed(self) -> Box> + where + Self: Sized + 'static, + { + Box::new(self) + } +} + +/// All things that implement the required traits can be treated as `AudioComponent`. +impl + Audio> AudioComponent for W {} diff --git a/crates/tek/src/jack/audio.rs b/crates/tek/src/jack/audio.rs new file mode 100644 index 00000000..b78c7e65 --- /dev/null +++ b/crates/tek/src/jack/audio.rs @@ -0,0 +1,78 @@ +use crate::*; + +/// Implement [Audio]: provide JACK callbacks. +#[macro_export] macro_rules! audio { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { + #[inline] fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control { $cb } + } + } +} + +/// Trait for thing that has a JACK process callback. +pub trait Audio: Send + Sync { + fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { + Control::Continue + } + fn callback ( + state: &Arc>, client: &Client, scope: &ProcessScope + ) -> Control where Self: Sized { + if let Ok(mut state) = state.write() { + state.process(client, scope) + } else { + Control::Quit + } + } +} + + +/// Trait for things that wrap a JACK client. +pub trait AudioEngine { + + fn transport (&self) -> Transport { + self.client().transport() + } + + fn port_by_name (&self, name: &str) -> Option> { + self.client().port_by_name(name) + } + + fn register_port (&self, name: &str, spec: PS) -> Usually> { + Ok(self.client().register_port(name, spec)?) + } + + fn client (&self) -> &Client; + + fn activate ( + self, + process: impl FnMut(&Arc>, &Client, &ProcessScope) -> Control + Send + 'static + ) -> Usually>> where Self: Send + Sync + 'static; + + fn thread_init (&self, _: &Client) {} + + unsafe fn shutdown (&mut self, _status: ClientStatus, _reason: &str) {} + + fn freewheel (&mut self, _: &Client, _enabled: bool) {} + + fn client_registration (&mut self, _: &Client, _name: &str, _reg: bool) {} + + fn port_registration (&mut self, _: &Client, _id: PortId, _reg: bool) {} + + fn ports_connected (&mut self, _: &Client, _a: PortId, _b: PortId, _are: bool) {} + + fn sample_rate (&mut self, _: &Client, _frames: Frames) -> Control { + Control::Continue + } + + fn port_rename (&mut self, _: &Client, _id: PortId, _old: &str, _new: &str) -> Control { + Control::Continue + } + + fn graph_reorder (&mut self, _: &Client) -> Control { + Control::Continue + } + + fn xrun (&mut self, _: &Client) -> Control { + Control::Continue + } +} diff --git a/crates/tek/src/jack/client.rs b/crates/tek/src/jack/client.rs new file mode 100644 index 00000000..046990bd --- /dev/null +++ b/crates/tek/src/jack/client.rs @@ -0,0 +1,61 @@ +use crate::*; + +/// Wraps [Client] or [DynamicAsyncClient] in place. +#[derive(Debug)] +pub enum JackClient { + /// Before activation. + Inactive(Client), + /// During activation. + Activating, + /// After activation. Must not be dropped for JACK thread to persist. + Active(DynamicAsyncClient), +} + +impl AudioEngine for JackClient { + fn client(&self) -> &Client { + match self { + Self::Inactive(ref client) => client, + Self::Activating => panic!("jack client has not finished activation"), + Self::Active(ref client) => client.as_client(), + } + } + fn activate( + self, + mut cb: impl FnMut(&Arc>, &Client, &ProcessScope) -> Control + Send + 'static, + ) -> Usually>> + where + Self: Send + Sync + 'static + { + let client = Client::from(self); + let state = Arc::new(RwLock::new(Self::Activating)); + let event = Box::new(move|_|{/*TODO*/}) as Box; + let events = Notifications(event); + let frame = Box::new({let state = state.clone(); move|c: &_, s: &_|cb(&state, c, s)}); + let frames = ClosureProcessHandler::new(frame as BoxedAudioHandler); + *state.write().unwrap() = Self::Active(client.activate_async(events, frames)?); + Ok(state) + } +} + +pub type DynamicAsyncClient = AsyncClient; + +pub type DynamicAudioHandler = ClosureProcessHandler<(), BoxedAudioHandler>; + +pub type BoxedAudioHandler = Box Control + Send>; + +impl JackClient { + pub fn new (name: &str) -> Usually { + let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; + Ok(Self::Inactive(client)) + } +} + +impl From for Client { + fn from (jack: JackClient) -> Client { + match jack { + JackClient::Inactive(client) => client, + JackClient::Activating => panic!("jack client still activating"), + JackClient::Active(_) => panic!("jack client already activated"), + } + } +} diff --git a/crates/tek/src/jack/from_jack.rs b/crates/tek/src/jack/from_jack.rs new file mode 100644 index 00000000..bdd1fd2e --- /dev/null +++ b/crates/tek/src/jack/from_jack.rs @@ -0,0 +1,13 @@ + + +/// Implement [TryFrom<&Arc>>]: create app state from wrapped JACK handle. +#[macro_export] macro_rules! from_jack { + (|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc>> for $Struct $(<$($L),*$($T),*>)? { + type Error = Box; + fn try_from ($jack: &Arc>) -> Usually { + Ok($cb) + } + } + }; +} diff --git a/crates/tek/src/jack/jack_event.rs b/crates/tek/src/jack/jack_event.rs new file mode 100644 index 00000000..b817099f --- /dev/null +++ b/crates/tek/src/jack/jack_event.rs @@ -0,0 +1,69 @@ +use crate::*; + +#[derive(Debug, Clone)] +/// Event enum for JACK events. +pub enum JackEvent { + ThreadInit, + Shutdown(ClientStatus, String), + Freewheel(bool), + SampleRate(Frames), + ClientRegistration(String, bool), + PortRegistration(PortId, bool), + PortRename(PortId, String, String), + PortsConnected(PortId, PortId, bool), + GraphReorder, + XRun, +} + +/// Notification handler used by the [Jack] factory +/// when constructing [JackDevice]s. +pub type DynamicNotifications = Notifications>; + +/// Generic notification handler that emits [JackEvent] +pub struct Notifications(pub T); + +impl NotificationHandler for Notifications { + fn thread_init(&self, _: &Client) { + self.0(JackEvent::ThreadInit); + } + + unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { + self.0(JackEvent::Shutdown(status, reason.into())); + } + + fn freewheel(&mut self, _: &Client, enabled: bool) { + self.0(JackEvent::Freewheel(enabled)); + } + + fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { + self.0(JackEvent::SampleRate(frames)); + Control::Quit + } + + fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { + self.0(JackEvent::ClientRegistration(name.into(), reg)); + } + + fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { + self.0(JackEvent::PortRegistration(id, reg)); + } + + fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + self.0(JackEvent::PortRename(id, old.into(), new.into())); + Control::Continue + } + + fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { + self.0(JackEvent::PortsConnected(a, b, are)); + } + + fn graph_reorder(&mut self, _: &Client) -> Control { + self.0(JackEvent::GraphReorder); + Control::Continue + } + + fn xrun(&mut self, _: &Client) -> Control { + self.0(JackEvent::XRun); + Control::Continue + } +} diff --git a/crates/tek/src/jack/ports.rs b/crates/tek/src/jack/ports.rs new file mode 100644 index 00000000..299f123e --- /dev/null +++ b/crates/tek/src/jack/ports.rs @@ -0,0 +1,39 @@ +use crate::*; + +/// Trait for things that may expose JACK ports. +pub trait Ports { + fn audio_ins(&self) -> Usually>> { + Ok(vec![]) + } + fn audio_outs(&self) -> Usually>> { + Ok(vec![]) + } + fn midi_ins(&self) -> Usually>> { + Ok(vec![]) + } + fn midi_outs(&self) -> Usually>> { + Ok(vec![]) + } +} + +fn register_ports( + client: &Client, + names: Vec, + spec: T, +) -> Usually>> { + names + .into_iter() + .try_fold(BTreeMap::new(), |mut ports, name| { + let port = client.register_port(&name, spec)?; + ports.insert(name, port); + Ok(ports) + }) +} + +fn query_ports(client: &Client, names: Vec) -> BTreeMap> { + names.into_iter().fold(BTreeMap::new(), |mut ports, name| { + let port = client.port_by_name(&name).unwrap(); + ports.insert(name, port); + ports + }) +} diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs index f27561eb..b570e28d 100644 --- a/crates/tek/src/lib.rs +++ b/crates/tek/src/lib.rs @@ -34,14 +34,20 @@ pub(crate) use tui::*; pub mod edn; pub(crate) use edn::*; +pub mod jack; +pub(crate) use jack::*; + +pub mod midi; +pub(crate) use midi::*; + testmod! { test } pub(crate) use clap::{self, Parser}; -pub use better_panic; +pub use ::better_panic; pub(crate) use better_panic::{Settings, Verbosity}; -pub use atomic_float; +pub use ::atomic_float; pub(crate) use atomic_float::*; pub(crate) use std::sync::{Arc, Mutex, RwLock}; @@ -58,28 +64,32 @@ pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem}; pub(crate) use std::cmp::{Ord, Eq, PartialEq}; pub(crate) use std::fmt::{Debug, Display}; -pub use crossterm; +pub use ::crossterm; pub(crate) use crossterm::{ExecutableCommand}; pub(crate) use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}; pub(crate) use crossterm::event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState}; -pub use ratatui; +pub use ::ratatui; pub(crate) use ratatui::{ prelude::{Style, Color, Buffer}, style::{Stylize, Modifier}, backend::{Backend, CrosstermBackend, ClearType} }; -pub use jack; -pub(crate) use jack::{ +pub use ::jack as libjack; +pub(crate) use ::jack::{ contrib::ClosureProcessHandler, - Client, ProcessScope, Control, CycleTimes, - Port, PortSpec, MidiIn, MidiOut, AudioIn, AudioOut, Unowned, + Client, AsyncClient, ClientOptions, ClientStatus, + ProcessScope, Control, CycleTimes, + Port, PortId, + PortSpec, MidiIn, MidiOut, AudioIn, AudioOut, Unowned, Transport, TransportState, MidiIter, RawMidi, + Frames, + NotificationHandler, }; -pub use midly; -pub(crate) use midly::{ +pub use ::midly; +pub(crate) use ::midly::{ Smf, MidiMessage, TrackEventKind, @@ -87,8 +97,8 @@ pub(crate) use midly::{ num::u7 }; -pub use palette; -pub(crate) use palette::{ +pub use ::palette; +pub(crate) use ::palette::{ *, convert::*, okhsl::* diff --git a/crates/tek/src/midi.rs b/crates/tek/src/midi.rs new file mode 100644 index 00000000..a7ed508f --- /dev/null +++ b/crates/tek/src/midi.rs @@ -0,0 +1,36 @@ +use crate::*; + +pub(crate) mod midi_note; pub(crate) use midi_note::*; +pub(crate) mod midi_in; pub(crate) use midi_in::*; +pub(crate) mod midi_out; pub(crate) use midi_out::*; +pub(crate) mod midi_player; pub(crate) use midi_player::*; +pub(crate) mod midi_launch; pub(crate) use midi_launch::*; +pub(crate) mod midi_play; pub(crate) use midi_play::*; +pub(crate) mod midi_rec; pub(crate) use midi_rec::*; + +/// Add "all notes off" to the start of a buffer. +pub fn all_notes_off (output: &mut [Vec>]) { + let mut buf = vec![]; + let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; + let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; + 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 }|( + time as usize, + LiveEvent::parse(bytes).unwrap(), + bytes + ))) +} + +/// Update notes_in array +pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { + match message { + MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } + MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, + _ => {} + } +} diff --git a/crates/tek/src/midi/midi_in.rs b/crates/tek/src/midi/midi_in.rs new file mode 100644 index 00000000..4750ca9e --- /dev/null +++ b/crates/tek/src/midi/midi_in.rs @@ -0,0 +1,10 @@ +use crate::*; + +/// Trait for thing that may receive MIDI. +pub trait HasMidiIns { + fn midi_ins (&self) -> &Vec>; + fn midi_ins_mut (&mut self) -> &mut Vec>; + fn has_midi_ins (&self) -> bool { + self.midi_ins().len() > 0 + } +} diff --git a/crates/tek/src/midi/midi_launch.rs b/crates/tek/src/midi/midi_launch.rs new file mode 100644 index 00000000..e680601a --- /dev/null +++ b/crates/tek/src/midi/midi_launch.rs @@ -0,0 +1,35 @@ +use crate::*; + +pub trait HasPlayPhrase: HasClock { + fn reset (&self) -> bool; + fn reset_mut (&mut self) -> &mut bool; + fn play_phrase (&self) -> &Option<(Moment, Option>>)>; + fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + fn next_phrase (&self) -> &Option<(Moment, Option>>)>; + fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + fn pulses_since_start (&self) -> Option { + if let Some((started, Some(_))) = self.play_phrase().as_ref() { + let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); + Some(elapsed) + } else { + None + } + } + fn pulses_since_start_looped (&self) -> Option { + if let Some((started, Some(phrase))) = self.play_phrase().as_ref() { + let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); + let length = phrase.read().unwrap().length.max(1); // prevent div0 on empty phrase + let elapsed = (elapsed as usize % length) as f64; + Some(elapsed) + } else { + None + } + } + fn enqueue_next (&mut self, phrase: Option<&Arc>>) { + let start = self.clock().next_launch_pulse() as f64; + let instant = Moment::from_pulse(&self.clock().timebase(), start); + let phrase = phrase.map(|p|p.clone()); + *self.next_phrase_mut() = Some((instant, phrase)); + *self.reset_mut() = true; + } +} diff --git a/crates/tek/src/midi/midi_note.rs b/crates/tek/src/midi/midi_note.rs new file mode 100644 index 00000000..abbc4bc0 --- /dev/null +++ b/crates/tek/src/midi/midi_note.rs @@ -0,0 +1,110 @@ +use crate::*; +use Ordering::Relaxed; + +pub trait MidiViewport: MidiRange + MidiPoint + HasSize { + /// Make sure cursor is within range + fn autoscroll (&self) { + let note_lo = self.note_lo(); + let note_axis = self.note_axis(); + let note_hi = self.note_hi(); + let note_point = self.note_point().min(127); + if note_point < note_lo { + self.set_note_lo(note_point); + } else if note_point > note_hi { + self.set_note_lo((note_lo + note_point).saturating_sub(note_hi)); + } + } + /// Make sure best usage of screen space is achieved by default + fn autozoom (&self) { + } +} + +#[derive(Debug, Clone)] +pub struct MidiRangeModel { + /// Length of visible time axis + pub time_axis: Arc, + /// Earliest time displayed + pub time_start: Arc, + /// Time step + pub time_zoom: Arc, + /// Auto rezoom to fit in time axis + pub time_lock: Arc, + /// Length of visible note axis + pub note_axis: Arc, + // Lowest note displayed + pub note_lo: Arc, +} +impl From<(usize, bool)> for MidiRangeModel { + fn from ((time_zoom, time_lock): (usize, bool)) -> Self { + Self { + note_axis: Arc::new(0.into()), + note_lo: Arc::new(0.into()), + time_axis: Arc::new(0.into()), + time_start: Arc::new(0.into()), + time_zoom: Arc::new(time_zoom.into()), + time_lock: Arc::new(time_lock.into()), + } + } +} +pub trait MidiRange { + fn time_zoom (&self) -> usize; + fn set_time_zoom (&mut self, x: usize); + fn time_lock (&self) -> bool; + fn set_time_lock (&self, x: bool); + fn time_start (&self) -> usize; + fn set_time_start (&self, x: usize); + fn note_lo (&self) -> usize; + fn set_note_lo (&self, x: usize); + fn note_axis (&self) -> usize; + fn time_axis (&self) -> usize; + fn note_hi (&self) -> usize { (self.note_lo() + self.note_axis().saturating_sub(1)).min(127) } + fn time_end (&self) -> usize { self.time_start() + self.time_axis() * self.time_zoom() } +} +impl MidiRange for MidiRangeModel { + fn time_zoom (&self) -> usize { self.time_zoom.load(Relaxed) } + fn set_time_zoom (&mut self, x: usize) { self.time_zoom.store(x, Relaxed); } + fn time_lock (&self) -> bool { self.time_lock.load(Relaxed) } + fn set_time_lock (&self, x: bool) { self.time_lock.store(x, Relaxed); } + fn time_start (&self) -> usize { self.time_start.load(Relaxed) } + fn set_time_start (&self, x: usize) { self.time_start.store(x, Relaxed); } + fn note_lo (&self) -> usize { self.note_lo.load(Relaxed).min(127) } + fn set_note_lo (&self, x: usize) { self.note_lo.store(x.min(127), Relaxed); } + fn note_axis (&self) -> usize { self.note_axis.load(Relaxed) } + fn time_axis (&self) -> usize { self.time_axis.load(Relaxed) } +} + +#[derive(Debug, Clone)] +pub struct MidiPointModel { + /// Time coordinate of cursor + pub time_point: Arc, + /// Note coordinate of cursor + pub note_point: Arc, + /// Length of note that will be inserted, in pulses + pub note_len: Arc, +} +impl Default for MidiPointModel { + fn default () -> Self { + Self { + time_point: Arc::new(0.into()), + note_point: Arc::new(36.into()), + note_len: Arc::new(24.into()), + } + } +} +pub trait MidiPoint { + fn note_len (&self) -> usize; + fn set_note_len (&self, x: usize); + fn note_point (&self) -> usize; + fn set_note_point (&self, x: usize); + fn time_point (&self) -> usize; + fn set_time_point (&self, x: usize); + fn note_end (&self) -> usize { self.note_point() + self.note_len() } +} +impl MidiPoint for MidiPointModel { + fn note_len (&self) -> usize { self.note_len.load(Relaxed)} + fn set_note_len (&self, x: usize) { self.note_len.store(x, Relaxed) } + fn note_point (&self) -> usize { self.note_point.load(Relaxed).min(127) } + fn set_note_point (&self, x: usize) { self.note_point.store(x.min(127), Relaxed) } + fn time_point (&self) -> usize { self.time_point.load(Relaxed) } + fn set_time_point (&self, x: usize) { self.time_point.store(x, Relaxed) } +} diff --git a/crates/tek/src/midi/midi_out.rs b/crates/tek/src/midi/midi_out.rs new file mode 100644 index 00000000..0010ef4d --- /dev/null +++ b/crates/tek/src/midi/midi_out.rs @@ -0,0 +1,12 @@ +use crate::*; + +/// Trait for thing that may output MIDI. +pub trait HasMidiOuts { + fn midi_outs (&self) -> &Vec>; + fn midi_outs_mut (&mut self) -> &mut Vec>; + fn has_midi_outs (&self) -> bool { + self.midi_outs().len() > 0 + } + /// Buffer for serializing a MIDI event. FIXME rename + fn midi_note (&mut self) -> &mut Vec; +} diff --git a/crates/tek/src/midi/midi_play.rs b/crates/tek/src/midi/midi_play.rs new file mode 100644 index 00000000..e32af28b --- /dev/null +++ b/crates/tek/src/midi/midi_play.rs @@ -0,0 +1,134 @@ +use crate::*; + +pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts { + + fn notes_out (&self) -> &Arc>; + + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + fn clear ( + &mut self, scope: &ProcessScope, out_buf: &mut Vec>>, reset: bool + ) { + for frame in &mut out_buf[0..scope.n_frames() as usize] { + frame.clear(); + } + if reset { + all_notes_off(out_buf); + } + } + + /// Output notes from phrase to MIDI output ports. + fn play ( + &mut self, scope: &ProcessScope, note_buf: &mut Vec, out_buf: &mut Vec>> + ) -> bool { + let mut next = false; + // Write MIDI events from currently playing phrase (if any) to MIDI output buffer + if self.clock().is_rolling() { + let sample0 = scope.last_frame_time() as usize; + let samples = scope.n_frames() as usize; + // If no phrase is playing, prepare for switchover immediately + next = self.play_phrase().is_none(); + let phrase = self.play_phrase(); + let started0 = &self.clock().started; + let timebase = self.clock().timebase(); + let notes_out = self.notes_out(); + let next_phrase = self.next_phrase(); + if let Some((started, phrase)) = phrase { + // First sample to populate. Greater than 0 means that the first + // pulse of the phrase falls somewhere in the middle of the chunk. + let sample = started.sample.get() as usize; + let sample = sample + started0.read().unwrap().as_ref().unwrap().sample.get() as usize; + let sample = sample0.saturating_sub(sample); + // Iterator that emits sample (index into output buffer at which to write MIDI event) + // paired with pulse (index into phrase from which to take the MIDI event) for each + // sample of the output buffer that corresponds to a MIDI pulse. + let pulses = timebase.pulses_between_samples(sample, sample + samples); + // Notes active during current chunk. + let notes = &mut notes_out.write().unwrap(); + for (sample, pulse) in pulses { + // If a next phrase is enqueued, and we're past the end of the current one, + // break the loop here (FIXME count pulse correctly) + next = next_phrase.is_some() && if let Some(ref phrase) = phrase { + pulse >= phrase.read().unwrap().length + } else { + true + }; + if next { + break + } + // If there's a currently playing phrase, output notes from it to buffer: + if let Some(ref phrase) = phrase { + // Source phrase from which the MIDI events will be taken. + let phrase = phrase.read().unwrap(); + // Phrase with zero length is not processed + if phrase.length > 0 { + // Current pulse index in source phrase + let pulse = pulse % phrase.length; + // Output each MIDI event from phrase at appropriate frames of output buffer: + for message in phrase.notes[pulse].iter() { + // Clear output buffer for this MIDI event. + note_buf.clear(); + // TODO: support MIDI channels other than CH1. + let channel = 0.into(); + // Serialize MIDI event into message buffer. + LiveEvent::Midi { channel, message: *message } + .write(note_buf) + .unwrap(); + // Append serialized message to output buffer. + out_buf[sample].push(note_buf.clone()); + // Update the list of currently held notes. + update_keys(&mut*notes, &message); + } + } + } + } + } + } + next + } + + /// Handle switchover from current to next playing phrase. + fn switchover ( + &mut self, scope: &ProcessScope, note_buf: &mut Vec, out_buf: &mut Vec>> + ) { + if self.clock().is_rolling() { + let sample0 = scope.last_frame_time() as usize; + //let samples = scope.n_frames() as usize; + if let Some((start_at, phrase)) = &self.next_phrase() { + let start = start_at.sample.get() as usize; + let sample = self.clock().started.read().unwrap().as_ref().unwrap().sample.get() as usize; + // If it's time to switch to the next phrase: + if start <= sample0.saturating_sub(sample) { + // Samples elapsed since phrase was supposed to start + let skipped = sample0 - start; + // Switch over to enqueued phrase + let started = Moment::from_sample(&self.clock().timebase(), start as f64); + *self.play_phrase_mut() = Some((started, phrase.clone())); + // Unset enqueuement (TODO: where to implement looping?) + *self.next_phrase_mut() = None + } + // TODO fill in remaining ticks of chunk from next phrase. + // ?? just call self.play(scope) again, since enqueuement is off ??? + self.play(scope, note_buf, out_buf); + // ?? or must it be with modified scope ?? + // likely not because start time etc + } + } + } + + /// Write a chunk of MIDI notes to the output buffer. + fn write ( + &mut self, scope: &ProcessScope, out_buf: &Vec>> + ) { + let samples = scope.n_frames() as usize; + for port in self.midi_outs_mut().iter_mut() { + let writer = &mut port.writer(scope); + for time in 0..samples { + for event in out_buf[time].iter() { + writer.write(&RawMidi { time: time as u32, bytes: &event }) + .expect(&format!("{event:?}")); + } + } + } + } +} diff --git a/crates/tek/src/midi/midi_player.rs b/crates/tek/src/midi/midi_player.rs new file mode 100644 index 00000000..e0eedd3c --- /dev/null +++ b/crates/tek/src/midi/midi_player.rs @@ -0,0 +1,255 @@ +use crate::*; + +pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {} + +impl MidiPlayerApi for PhrasePlayerModel {} + +pub trait HasPlayer { + fn player (&self) -> &impl MidiPlayerApi; + fn player_mut (&mut self) -> &mut impl MidiPlayerApi; +} + +#[macro_export] macro_rules! has_player { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? { + fn player (&$self) -> &impl MidiPlayerApi { &$cb } + fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb } + } + } +} + +/// Contains state for playing a phrase +pub struct PhrasePlayerModel { + /// State of clock and playhead + pub(crate) clock: ClockModel, + /// Start time and phrase being played + pub(crate) play_phrase: Option<(Moment, Option>>)>, + /// Start time and next phrase + pub(crate) next_phrase: Option<(Moment, Option>>)>, + /// Play input through output. + pub(crate) monitoring: bool, + /// Write input to sequence. + pub(crate) recording: bool, + /// Overdub input to sequence. + pub(crate) overdub: bool, + /// Send all notes off + pub(crate) reset: bool, // TODO?: after Some(nframes) + /// Record from MIDI ports to current sequence. + pub midi_ins: Vec>, + /// Play from current sequence to MIDI ports + pub midi_outs: Vec>, + /// Notes currently held at input + pub(crate) notes_in: Arc>, + /// Notes currently held at output + pub(crate) notes_out: Arc>, + /// MIDI output buffer + pub note_buf: Vec, +} + +impl std::fmt::Debug for PhrasePlayerModel { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("PhrasePlayerModel") + .field("clock", &self.clock) + .field("play_phrase", &self.play_phrase) + .field("next_phrase", &self.next_phrase) + .finish() + } +} + +impl From<&ClockModel> for PhrasePlayerModel { + fn from (clock: &ClockModel) -> Self { + Self { + clock: clock.clone(), + midi_ins: vec![], + midi_outs: vec![], + note_buf: vec![0;8], + reset: true, + recording: false, + monitoring: false, + overdub: false, + play_phrase: None, + next_phrase: None, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + } + } +} + +impl From<(&ClockModel, &Arc>)> for PhrasePlayerModel { + fn from ((clock, phrase): (&ClockModel, &Arc>)) -> Self { + let mut model = Self::from(clock); + model.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone()))); + model + } +} + +has_clock!(|self:PhrasePlayerModel|&self.clock); + +impl HasMidiIns for PhrasePlayerModel { + fn midi_ins (&self) -> &Vec> { + &self.midi_ins + } + fn midi_ins_mut (&mut self) -> &mut Vec> { + &mut self.midi_ins + } +} + +impl HasMidiOuts for PhrasePlayerModel { + fn midi_outs (&self) -> &Vec> { + &self.midi_outs + } + fn midi_outs_mut (&mut self) -> &mut Vec> { + &mut self.midi_outs + } + fn midi_note (&mut self) -> &mut Vec { + &mut self.note_buf + } +} + +/// Hosts the JACK callback for a single MIDI player +pub struct PlayerAudio<'a, T: MidiPlayerApi>( + /// Player + pub &'a mut T, + /// Note buffer + pub &'a mut Vec, + /// Note chunk buffer + pub &'a mut Vec>>, +); + +/// JACK process callback for a sequencer's phrase player/recorder. +impl<'a, T: MidiPlayerApi> Audio for PlayerAudio<'a, T> { + fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + let model = &mut self.0; + let note_buf = &mut self.1; + let midi_buf = &mut self.2; + // Clear output buffer(s) + model.clear(scope, midi_buf, false); + // Write chunk of phrase to output, handle switchover + if model.play(scope, note_buf, midi_buf) { + model.switchover(scope, note_buf, midi_buf); + } + if model.has_midi_ins() { + if model.recording() || model.monitoring() { + // Record and/or monitor input + model.record(scope, midi_buf) + } else if model.has_midi_outs() && model.monitoring() { + // Monitor input to output + model.monitor(scope, midi_buf) + } + } + // Write to output port(s) + model.write(scope, midi_buf); + Control::Continue + } +} + +impl MidiRecordApi for PhrasePlayerModel { + fn recording (&self) -> bool { + self.recording + } + fn recording_mut (&mut self) -> &mut bool { + &mut self.recording + } + fn monitoring (&self) -> bool { + self.monitoring + } + fn monitoring_mut (&mut self) -> &mut bool { + &mut self.monitoring + } + fn overdub (&self) -> bool { + self.overdub + } + fn overdub_mut (&mut self) -> &mut bool { + &mut self.overdub + } + fn notes_in (&self) -> &Arc> { + &self.notes_in + } +} + +impl MidiPlaybackApi for PhrasePlayerModel { + fn notes_out (&self) -> &Arc> { + &self.notes_in + } +} + +impl HasPlayPhrase for PhrasePlayerModel { + fn reset (&self) -> bool { + self.reset + } + fn reset_mut (&mut self) -> &mut bool { + &mut self.reset + } + fn play_phrase (&self) -> &Option<(Moment, Option>>)> { + &self.play_phrase + } + fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.play_phrase + } + fn next_phrase (&self) -> &Option<(Moment, Option>>)> { + &self.next_phrase + } + fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.next_phrase + } +} + +//#[derive(Debug)] +//pub struct MIDIPlayer { + ///// Global timebase + //pub clock: Arc, + ///// Start time and phrase being played + //pub play_phrase: Option<(Moment, Option>>)>, + ///// Start time and next phrase + //pub next_phrase: Option<(Moment, Option>>)>, + ///// Play input through output. + //pub monitoring: bool, + ///// Write input to sequence. + //pub recording: bool, + ///// Overdub input to sequence. + //pub overdub: bool, + ///// Send all notes off + //pub reset: bool, // TODO?: after Some(nframes) + ///// Record from MIDI ports to current sequence. + //pub midi_inputs: Vec>, + ///// Play from current sequence to MIDI ports + //pub midi_outputs: Vec>, + ///// MIDI output buffer + //pub midi_note: Vec, + ///// MIDI output buffer + //pub midi_chunk: Vec>>, + ///// Notes currently held at input + //pub notes_in: Arc>, + ///// Notes currently held at output + //pub notes_out: Arc>, +//} + +///// Methods used primarily by the process callback +//impl MIDIPlayer { + //pub fn new ( + //jack: &Arc>, + //clock: &Arc, + //name: &str + //) -> Usually { + //let jack = jack.read().unwrap(); + //Ok(Self { + //clock: clock.clone(), + //phrase: None, + //next_phrase: None, + //notes_in: Arc::new(RwLock::new([false;128])), + //notes_out: Arc::new(RwLock::new([false;128])), + //monitoring: false, + //recording: false, + //overdub: true, + //reset: true, + //midi_note: Vec::with_capacity(8), + //midi_chunk: vec![Vec::with_capacity(16);16384], + //midi_outputs: vec![ + //jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())? + //], + //midi_inputs: vec![ + //jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())? + //], + //}) + //} +//} diff --git a/crates/tek/src/midi/midi_rec.rs b/crates/tek/src/midi/midi_rec.rs new file mode 100644 index 00000000..22a58f74 --- /dev/null +++ b/crates/tek/src/midi/midi_rec.rs @@ -0,0 +1,75 @@ +use crate::*; + +pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns { + fn notes_in (&self) -> &Arc>; + + fn recording (&self) -> bool; + fn recording_mut (&mut self) -> &mut bool; + fn toggle_record (&mut self) { + *self.recording_mut() = !self.recording(); + } + fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { + let sample0 = scope.last_frame_time() as usize; + // For highlighting keys and note repeat + let notes_in = self.notes_in().clone(); + if self.clock().is_rolling() { + if let Some((started, ref phrase)) = self.play_phrase().clone() { + let start = started.sample.get() as usize; + let quant = self.clock().quant.get(); + let timebase = self.clock().timebase().clone(); + let monitoring = self.monitoring(); + let recording = self.recording(); + for input in self.midi_ins_mut().iter() { + for (sample, event, bytes) in parse_midi_input(input.iter(scope)) { + if let LiveEvent::Midi { message, .. } = event { + if monitoring { + midi_buf[sample].push(bytes.to_vec()) + } + if recording { + if let Some(phrase) = phrase { + let mut phrase = phrase.write().unwrap(); + let length = phrase.length; + phrase.record_event({ + let sample = (sample0 + sample - start) as f64; + let pulse = timebase.samples_to_pulse(sample); + let quantized = (pulse / quant).round() * quant; + let looped = quantized as usize % length; + looped + }, message); + } + } + update_keys(&mut*notes_in.write().unwrap(), &message); + } + } + } + } + if let Some((start_at, phrase)) = &self.next_phrase() { + // TODO switch to next phrase and record into it + } + } + } + + fn monitoring (&self) -> bool; + fn monitoring_mut (&mut self) -> &mut bool; + fn toggle_monitor (&mut self) { + *self.monitoring_mut() = !self.monitoring(); + } + fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { + // For highlighting keys and note repeat + let notes_in = self.notes_in().clone(); + for input in self.midi_ins_mut().iter() { + for (sample, event, bytes) in parse_midi_input(input.iter(scope)) { + if let LiveEvent::Midi { message, .. } = event { + midi_buf[sample].push(bytes.to_vec()); + update_keys(&mut*notes_in.write().unwrap(), &message); + } + } + } + } + + fn overdub (&self) -> bool; + fn overdub_mut (&mut self) -> &mut bool; + fn toggle_overdub (&mut self) { + *self.overdub_mut() = !self.overdub(); + } +} diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 67c0fc84..e22156fa 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -1121,3 +1121,66 @@ pub fn arranger_content_horizontal ( //) //) //} + +//impl Command for ArrangerSceneCommand { +//} + //Edit(phrase) => { state.state.phrase = phrase.clone() }, + //ToggleViewMode => { state.state.mode.to_next(); }, + //Delete => { state.state.delete(); }, + //Activate => { state.state.activate(); }, + //ZoomIn => { state.state.zoom_in(); }, + //ZoomOut => { state.state.zoom_out(); }, + //MoveBack => { state.state.move_back(); }, + //MoveForward => { state.state.move_forward(); }, + //RandomColor => { state.state.randomize_color(); }, + //Put => { state.state.phrase_put(); }, + //Get => { state.state.phrase_get(); }, + //AddScene => { state.state.scene_add(None, None)?; }, + //AddTrack => { state.state.track_add(None, None)?; }, + //ToggleLoop => { state.state.toggle_loop() }, + //pub fn zoom_in (&mut self) { + //if let ArrangerEditorMode::Vertical(factor) = self.mode { + //self.mode = ArrangerEditorMode::Vertical(factor + 1) + //} + //} + //pub fn zoom_out (&mut self) { + //if let ArrangerEditorMode::Vertical(factor) = self.mode { + //self.mode = ArrangerEditorMode::Vertical(factor.saturating_sub(1)) + //} + //} + //pub fn move_back (&mut self) { + //match self.selected { + //ArrangerEditorFocus::Scene(s) => { + //if s > 0 { + //self.scenes.swap(s, s - 1); + //self.selected = ArrangerEditorFocus::Scene(s - 1); + //} + //}, + //ArrangerEditorFocus::Track(t) => { + //if t > 0 { + //self.tracks.swap(t, t - 1); + //self.selected = ArrangerEditorFocus::Track(t - 1); + //// FIXME: also swap clip order in scenes + //} + //}, + //_ => todo!("arrangement: move forward") + //} + //} + //pub fn move_forward (&mut self) { + //match self.selected { + //ArrangerEditorFocus::Scene(s) => { + //if s < self.scenes.len().saturating_sub(1) { + //self.scenes.swap(s, s + 1); + //self.selected = ArrangerEditorFocus::Scene(s + 1); + //} + //}, + //ArrangerEditorFocus::Track(t) => { + //if t < self.tracks.len().saturating_sub(1) { + //self.tracks.swap(t, t + 1); + //self.selected = ArrangerEditorFocus::Track(t + 1); + //// FIXME: also swap clip order in scenes + //} + //}, + //_ => todo!("arrangement: move forward") + //} + //} From 61ab472e327828e4727388bd9729b49c063bb2f4 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 15:56:29 +0100 Subject: [PATCH 022/905] refactor core::time --- crates/tek/src/api.rs | 1 - crates/tek/src/api/note.rs | 0 crates/tek/src/api/timeline.ts | 0 crates/tek/src/core.rs | 4 ---- crates/tek/src/core/pitch.rs | 20 ----------------- crates/tek/src/lib.rs | 31 +++++++++------------------ crates/tek/src/midi.rs | 21 ++++++++++++++++++ crates/tek/src/{core => }/space.rs | 0 crates/tek/src/{core => }/time.rs | 4 ++++ crates/tek/src/{api => time}/clock.rs | 0 crates/tek/src/{core => time}/perf.rs | 0 crates/tek/src/tui/app_sequencer.rs | 3 ++- crates/tek/src/tui/app_transport.rs | 2 +- 13 files changed, 38 insertions(+), 48 deletions(-) delete mode 100644 crates/tek/src/api/note.rs delete mode 100644 crates/tek/src/api/timeline.ts delete mode 100644 crates/tek/src/core/pitch.rs rename crates/tek/src/{core => }/space.rs (100%) rename crates/tek/src/{core => }/time.rs (99%) rename crates/tek/src/{api => time}/clock.rs (100%) rename crates/tek/src/{core => time}/perf.rs (100%) diff --git a/crates/tek/src/api.rs b/crates/tek/src/api.rs index 4a83fd07..cc13d65c 100644 --- a/crates/tek/src/api.rs +++ b/crates/tek/src/api.rs @@ -1,5 +1,4 @@ mod phrase; pub(crate) use phrase::*; -mod clock; pub(crate) use clock::*; mod scene; pub(crate) use scene::*; mod track; pub(crate) use track::*; mod sampler; pub(crate) use sampler::*; diff --git a/crates/tek/src/api/note.rs b/crates/tek/src/api/note.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/tek/src/api/timeline.ts b/crates/tek/src/api/timeline.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/tek/src/core.rs b/crates/tek/src/core.rs index a7cb629a..d90e39c8 100644 --- a/crates/tek/src/core.rs +++ b/crates/tek/src/core.rs @@ -4,10 +4,6 @@ pub(crate) mod engine; pub(crate) use engine::*; pub(crate) mod focus; pub(crate) use focus::*; pub(crate) mod input; pub(crate) use input::*; pub(crate) mod output; pub(crate) use output::*; -pub(crate) mod perf; pub(crate) use perf::*; -pub(crate) mod pitch; pub(crate) use pitch::*; -pub(crate) mod space; pub(crate) use space::*; -pub(crate) mod time; pub(crate) use time::*; pub use self::{ engine::Engine, diff --git a/crates/tek/src/core/pitch.rs b/crates/tek/src/core/pitch.rs deleted file mode 100644 index e442c60f..00000000 --- a/crates/tek/src/core/pitch.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub fn to_note_name (n: usize) -> &'static str { - if n > 127 { - panic!("to_note_name({n}): must be 0-127"); - } - MIDI_NOTE_NAMES[n] -} - -pub const MIDI_NOTE_NAMES: [&'static str;128] = [ - "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", - "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", - "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", - "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", - "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", - "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", - "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6", - "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", - "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", - "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", - "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", -]; diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs index b570e28d..f63205e5 100644 --- a/crates/tek/src/lib.rs +++ b/crates/tek/src/lib.rs @@ -1,3 +1,13 @@ +pub mod core; pub(crate) use self::core::*; +pub mod layout; pub(crate) use self::layout::*; +pub mod api; pub(crate) use self::api::*; +pub mod tui; pub(crate) use self::tui::*; +pub mod edn; pub(crate) use self::edn::*; +pub mod jack; pub(crate) use self::jack::*; +pub mod midi; pub(crate) use self::midi::*; +pub mod time; pub(crate) use self::time::*; +pub mod space; pub(crate) use self::space::*; + /// Standard result type. pub type Usually = Result>; @@ -19,27 +29,6 @@ pub type Perhaps = Result, Box>; ($($name:ident)*) => { $(#[cfg(test)] mod $name;)* }; } -pub mod core; -pub(crate) use core::*; - -pub mod layout; pub(crate) -use layout::*; - -pub mod api; -pub(crate) use api::*; - -pub mod tui; -pub(crate) use tui::*; - -pub mod edn; -pub(crate) use edn::*; - -pub mod jack; -pub(crate) use jack::*; - -pub mod midi; -pub(crate) use midi::*; - testmod! { test } pub(crate) use clap::{self, Parser}; diff --git a/crates/tek/src/midi.rs b/crates/tek/src/midi.rs index a7ed508f..db92d43f 100644 --- a/crates/tek/src/midi.rs +++ b/crates/tek/src/midi.rs @@ -34,3 +34,24 @@ pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { _ => {} } } + +pub fn to_note_name (n: usize) -> &'static str { + if n > 127 { + panic!("to_note_name({n}): must be 0-127"); + } + MIDI_NOTE_NAMES[n] +} + +pub const MIDI_NOTE_NAMES: [&'static str;128] = [ + "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", + "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", + "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", + "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", + "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", + "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", + "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6", + "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", + "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", + "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", + "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", +]; diff --git a/crates/tek/src/core/space.rs b/crates/tek/src/space.rs similarity index 100% rename from crates/tek/src/core/space.rs rename to crates/tek/src/space.rs diff --git a/crates/tek/src/core/time.rs b/crates/tek/src/time.rs similarity index 99% rename from crates/tek/src/core/time.rs rename to crates/tek/src/time.rs index 6677431f..c8e61490 100644 --- a/crates/tek/src/core/time.rs +++ b/crates/tek/src/time.rs @@ -1,3 +1,7 @@ +pub(crate) mod perf; pub(crate) use perf::*; +pub(crate) mod clock; pub(crate) use clock::*; +pub(crate) use self::clock::ClockCommand; + use crate::*; use std::iter::Iterator; diff --git a/crates/tek/src/api/clock.rs b/crates/tek/src/time/clock.rs similarity index 100% rename from crates/tek/src/api/clock.rs rename to crates/tek/src/time/clock.rs diff --git a/crates/tek/src/core/perf.rs b/crates/tek/src/time/perf.rs similarity index 100% rename from crates/tek/src/core/perf.rs rename to crates/tek/src/time/perf.rs diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index cf2a2fdb..16b62cab 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -1,4 +1,5 @@ -use crate::{*, api::ClockCommand::{Play, Pause}}; +use crate::*; +use ClockCommand::{Play, Pause}; use KeyCode::{Tab, BackTab, Char}; use SequencerCommand::*; use PhraseCommand::*; diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index 535bad5a..f368a621 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -1,5 +1,5 @@ use crate::*; -use crate::api::ClockCommand::{Play, Pause, SetBpm, SetQuant, SetSync}; +use ClockCommand::{Play, Pause, SetBpm, SetQuant, SetSync}; use TransportCommand::{Focus, Clock}; use FocusCommand::{Next, Prev}; use KeyCode::{Enter, Left, Right, Char}; From f1a8d9e84686ff8dbf2e24f67246f84242a62939 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 16:07:46 +0100 Subject: [PATCH 023/905] refactor core::space --- crates/tek/src/core/engine.rs | 5 - crates/tek/src/space.rs | 165 +------------- crates/tek/src/space/area.rs | 73 ++++++ crates/tek/src/space/coord.rs | 37 ++++ crates/tek/src/space/direction.rs | 28 +++ crates/tek/src/space/size.rs | 26 +++ crates/tek/src/time.rs | 357 +----------------------------- crates/tek/src/time/bpm.rs | 5 + crates/tek/src/time/moment.rs | 69 ++++++ crates/tek/src/time/ppq.rs | 42 ++++ crates/tek/src/time/quant.rs | 25 +++ crates/tek/src/time/sr.rs | 27 +++ crates/tek/src/time/timebase.rs | 112 ++++++++++ crates/tek/src/time/unit.rs | 61 +++++ crates/tek/src/time/usec.rs | 13 ++ 15 files changed, 532 insertions(+), 513 deletions(-) create mode 100644 crates/tek/src/space/area.rs create mode 100644 crates/tek/src/space/coord.rs create mode 100644 crates/tek/src/space/direction.rs create mode 100644 crates/tek/src/space/size.rs create mode 100644 crates/tek/src/time/bpm.rs create mode 100644 crates/tek/src/time/moment.rs create mode 100644 crates/tek/src/time/ppq.rs create mode 100644 crates/tek/src/time/quant.rs create mode 100644 crates/tek/src/time/sr.rs create mode 100644 crates/tek/src/time/timebase.rs create mode 100644 crates/tek/src/time/unit.rs create mode 100644 crates/tek/src/time/usec.rs diff --git a/crates/tek/src/core/engine.rs b/crates/tek/src/core/engine.rs index 10effc1b..fb9ef473 100644 --- a/crates/tek/src/core/engine.rs +++ b/crates/tek/src/core/engine.rs @@ -1,10 +1,5 @@ use crate::*; -/// Entry point for main loop -pub trait App { - fn run (self, context: T) -> Usually; -} - /// Platform backend. pub trait Engine: Send + Sync + Sized { /// Input event type diff --git a/crates/tek/src/space.rs b/crates/tek/src/space.rs index 490dcc60..bb2ca859 100644 --- a/crates/tek/src/space.rs +++ b/crates/tek/src/space.rs @@ -1,164 +1,7 @@ -use crate::*; - -/// Standard numeric type. -pub trait Coordinate: Send + Sync + Copy - + Add - + Sub - + Mul - + Div - + Ord + PartialEq + Eq - + Debug + Display + Default - + From + Into - + Into - + Into -{ - fn minus (self, other: Self) -> Self { - if self >= other { - self - other - } else { - 0.into() - } - } - fn ZERO () -> Self { - 0.into() - } -} - -impl Coordinate for T where T: Send + Sync + Copy - + Add - + Sub - + Mul - + Div - + Ord + PartialEq + Eq - + Debug + Display + Default - + From + Into - + Into - + Into -{} +pub(crate) mod coord; pub(crate) use coord::*; +pub(crate) mod size; pub(crate) use size::*; +pub(crate) mod area; pub(crate) use area::*; +pub(crate) mod direction; pub(crate) use direction::*; // TODO: return impl Point and impl Size instead of [N;x] // to disambiguate between usage of 2-"tuple"s - -pub trait Size { - fn x (&self) -> N; - fn y (&self) -> N; - #[inline] fn w (&self) -> N { self.x() } - #[inline] fn h (&self) -> N { self.y() } - #[inline] fn wh (&self) -> [N;2] { [self.x(), self.y()] } - #[inline] fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w.into()), self.h()] } - #[inline] fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h.into())] } - #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { - if self.w() < w || self.h() < h { - Err(format!("min {w}x{h}").into()) - } else { - Ok(self) - } - } -} -impl Size for (N, N) { - fn x (&self) -> N { self.0 } - fn y (&self) -> N { self.1 } -} -impl Size for [N;2] { - fn x (&self) -> N { self[0] } - fn y (&self) -> N { self[1] } -} - -pub trait Area: Copy { - fn x (&self) -> N; - fn y (&self) -> N; - fn w (&self) -> N; - fn h (&self) -> N; - fn x2 (&self) -> N { self.x() + self.w() } - fn y2 (&self) -> N { self.y() + self.h() } - #[inline] fn wh (&self) -> [N;2] { [self.w(), self.h()] } - #[inline] fn xywh (&self) -> [N;4] { [self.x(), self.y(), self.w(), self.h()] } - #[inline] fn lrtb (&self) -> [N;4] { [self.x(), self.x2(), self.y(), self.y2()] } - #[inline] fn push_x (&self, x: N) -> [N;4] { [self.x() + x, self.y(), self.w(), self.h()] } - #[inline] fn push_y (&self, y: N) -> [N;4] { [self.x(), self.y() + y, self.w(), self.h()] } - #[inline] fn shrink_x (&self, x: N) -> [N;4] { - [self.x(), self.y(), self.w().minus(x), self.h()] - } - #[inline] fn shrink_y (&self, y: N) -> [N;4] { - [self.x(), self.y(), self.w(), self.h().minus(y)] - } - #[inline] fn set_w (&self, w: N) -> [N;4] { [self.x(), self.y(), w, self.h()] } - #[inline] fn set_h (&self, h: N) -> [N;4] { [self.x(), self.y(), self.w(), h] } - #[inline] fn clip_h (&self, h: N) -> [N;4] { - [self.x(), self.y(), self.w(), self.h().min(h.into())] - } - #[inline] fn clip_w (&self, w: N) -> [N;4] { - [self.x(), self.y(), self.w().min(w.into()), self.h()] - } - #[inline] fn clip (&self, wh: impl Size) -> [N;4] { - [self.x(), self.y(), wh.w(), wh.h()] - } - #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { - if self.w() < w || self.h() < h { - Err(format!("min {w}x{h}").into()) - } else { - Ok(self) - } - } - #[inline] fn split_fixed (&self, direction: Direction, a: N) -> ([N;4],[N;4]) { - match direction { - Direction::Up => ( - [self.x(), (self.y()+self.h()).minus(a), self.w(), a], - [self.x(), self.y(), self.w(), self.h().minus(a)], - ), - Direction::Down => ( - [self.x(), self.y(), self.w(), a], - [self.x(), self.y() + a, self.w(), self.h().minus(a)], - ), - Direction::Right => ( - [self.x(), self.y(), a, self.h()], - [self.x() + a, self.y(), self.w().minus(a), self.h()], - ), - Direction::Left => ( - [self.x() + self.w() - a, self.y(), a, self.h()], - [self.x(), self.y(), self.w() - a, self.h()], - ), - } - } -} - -impl Area for (N, N, N, N) { - #[inline] fn x (&self) -> N { self.0 } - #[inline] fn y (&self) -> N { self.1 } - #[inline] fn w (&self) -> N { self.2 } - #[inline] fn h (&self) -> N { self.3 } -} - -impl Area for [N;4] { - #[inline] fn x (&self) -> N { self[0] } - #[inline] fn y (&self) -> N { self[1] } - #[inline] fn w (&self) -> N { self[2] } - #[inline] fn h (&self) -> N { self[3] } -} - -#[derive(Copy, Clone, PartialEq)] -pub enum Direction { Up, Down, Left, Right, } -impl Direction { - pub fn is_up (&self) -> bool { match self { Self::Up => true, _ => false } } - pub fn is_down (&self) -> bool { match self { Self::Down => true, _ => false } } - pub fn is_left (&self) -> bool { match self { Self::Left => true, _ => false } } - pub fn is_right (&self) -> bool { match self { Self::Right => true, _ => false } } - /// Return next direction clockwise - pub fn cw (&self) -> Self { - match self { - Self::Up => Self::Right, - Self::Down => Self::Left, - Self::Left => Self::Up, - Self::Right => Self::Down, - } - } - /// Return next direction counterclockwise - pub fn ccw (&self) -> Self { - match self { - Self::Up => Self::Left, - Self::Down => Self::Right, - Self::Left => Self::Down, - Self::Right => Self::Up, - } - } -} diff --git a/crates/tek/src/space/area.rs b/crates/tek/src/space/area.rs new file mode 100644 index 00000000..c1c8d350 --- /dev/null +++ b/crates/tek/src/space/area.rs @@ -0,0 +1,73 @@ +use crate::*; + +pub trait Area: Copy { + fn x (&self) -> N; + fn y (&self) -> N; + fn w (&self) -> N; + fn h (&self) -> N; + fn x2 (&self) -> N { self.x() + self.w() } + fn y2 (&self) -> N { self.y() + self.h() } + #[inline] fn wh (&self) -> [N;2] { [self.w(), self.h()] } + #[inline] fn xywh (&self) -> [N;4] { [self.x(), self.y(), self.w(), self.h()] } + #[inline] fn lrtb (&self) -> [N;4] { [self.x(), self.x2(), self.y(), self.y2()] } + #[inline] fn push_x (&self, x: N) -> [N;4] { [self.x() + x, self.y(), self.w(), self.h()] } + #[inline] fn push_y (&self, y: N) -> [N;4] { [self.x(), self.y() + y, self.w(), self.h()] } + #[inline] fn shrink_x (&self, x: N) -> [N;4] { + [self.x(), self.y(), self.w().minus(x), self.h()] + } + #[inline] fn shrink_y (&self, y: N) -> [N;4] { + [self.x(), self.y(), self.w(), self.h().minus(y)] + } + #[inline] fn set_w (&self, w: N) -> [N;4] { [self.x(), self.y(), w, self.h()] } + #[inline] fn set_h (&self, h: N) -> [N;4] { [self.x(), self.y(), self.w(), h] } + #[inline] fn clip_h (&self, h: N) -> [N;4] { + [self.x(), self.y(), self.w(), self.h().min(h.into())] + } + #[inline] fn clip_w (&self, w: N) -> [N;4] { + [self.x(), self.y(), self.w().min(w.into()), self.h()] + } + #[inline] fn clip (&self, wh: impl Size) -> [N;4] { + [self.x(), self.y(), wh.w(), wh.h()] + } + #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { + if self.w() < w || self.h() < h { + Err(format!("min {w}x{h}").into()) + } else { + Ok(self) + } + } + #[inline] fn split_fixed (&self, direction: Direction, a: N) -> ([N;4],[N;4]) { + match direction { + Direction::Up => ( + [self.x(), (self.y()+self.h()).minus(a), self.w(), a], + [self.x(), self.y(), self.w(), self.h().minus(a)], + ), + Direction::Down => ( + [self.x(), self.y(), self.w(), a], + [self.x(), self.y() + a, self.w(), self.h().minus(a)], + ), + Direction::Right => ( + [self.x(), self.y(), a, self.h()], + [self.x() + a, self.y(), self.w().minus(a), self.h()], + ), + Direction::Left => ( + [self.x() + self.w() - a, self.y(), a, self.h()], + [self.x(), self.y(), self.w() - a, self.h()], + ), + } + } +} + +impl Area for (N, N, N, N) { + #[inline] fn x (&self) -> N { self.0 } + #[inline] fn y (&self) -> N { self.1 } + #[inline] fn w (&self) -> N { self.2 } + #[inline] fn h (&self) -> N { self.3 } +} + +impl Area for [N;4] { + #[inline] fn x (&self) -> N { self[0] } + #[inline] fn y (&self) -> N { self[1] } + #[inline] fn w (&self) -> N { self[2] } + #[inline] fn h (&self) -> N { self[3] } +} diff --git a/crates/tek/src/space/coord.rs b/crates/tek/src/space/coord.rs new file mode 100644 index 00000000..7feac7dc --- /dev/null +++ b/crates/tek/src/space/coord.rs @@ -0,0 +1,37 @@ +use crate::*; + +/// Standard numeric type. +pub trait Coordinate: Send + Sync + Copy + + Add + + Sub + + Mul + + Div + + Ord + PartialEq + Eq + + Debug + Display + Default + + From + Into + + Into + + Into +{ + fn minus (self, other: Self) -> Self { + if self >= other { + self - other + } else { + 0.into() + } + } + fn ZERO () -> Self { + 0.into() + } +} + +impl Coordinate for T where T: Send + Sync + Copy + + Add + + Sub + + Mul + + Div + + Ord + PartialEq + Eq + + Debug + Display + Default + + From + Into + + Into + + Into +{} diff --git a/crates/tek/src/space/direction.rs b/crates/tek/src/space/direction.rs new file mode 100644 index 00000000..7ad3eb8d --- /dev/null +++ b/crates/tek/src/space/direction.rs @@ -0,0 +1,28 @@ +use crate::*; + +#[derive(Copy, Clone, PartialEq)] +pub enum Direction { Up, Down, Left, Right, } +impl Direction { + pub fn is_up (&self) -> bool { match self { Self::Up => true, _ => false } } + pub fn is_down (&self) -> bool { match self { Self::Down => true, _ => false } } + pub fn is_left (&self) -> bool { match self { Self::Left => true, _ => false } } + pub fn is_right (&self) -> bool { match self { Self::Right => true, _ => false } } + /// Return next direction clockwise + pub fn cw (&self) -> Self { + match self { + Self::Up => Self::Right, + Self::Down => Self::Left, + Self::Left => Self::Up, + Self::Right => Self::Down, + } + } + /// Return next direction counterclockwise + pub fn ccw (&self) -> Self { + match self { + Self::Up => Self::Left, + Self::Down => Self::Right, + Self::Left => Self::Down, + Self::Right => Self::Up, + } + } +} diff --git a/crates/tek/src/space/size.rs b/crates/tek/src/space/size.rs new file mode 100644 index 00000000..5fe0ed40 --- /dev/null +++ b/crates/tek/src/space/size.rs @@ -0,0 +1,26 @@ +use crate::*; + +pub trait Size { + fn x (&self) -> N; + fn y (&self) -> N; + #[inline] fn w (&self) -> N { self.x() } + #[inline] fn h (&self) -> N { self.y() } + #[inline] fn wh (&self) -> [N;2] { [self.x(), self.y()] } + #[inline] fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w.into()), self.h()] } + #[inline] fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h.into())] } + #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { + if self.w() < w || self.h() < h { + Err(format!("min {w}x{h}").into()) + } else { + Ok(self) + } + } +} +impl Size for (N, N) { + fn x (&self) -> N { self.0 } + fn y (&self) -> N { self.1 } +} +impl Size for [N;2] { + fn x (&self) -> N { self[0] } + fn y (&self) -> N { self[1] } +} diff --git a/crates/tek/src/time.rs b/crates/tek/src/time.rs index c8e61490..58c3c798 100644 --- a/crates/tek/src/time.rs +++ b/crates/tek/src/time.rs @@ -1,350 +1,13 @@ -pub(crate) mod perf; pub(crate) use perf::*; -pub(crate) mod clock; pub(crate) use clock::*; -pub(crate) use self::clock::ClockCommand; - -use crate::*; -use std::iter::Iterator; - -pub const DEFAULT_PPQ: f64 = 96.0; -/// FIXME: remove this and use PPQ from timebase everywhere: -pub const PPQ: usize = 96; - -/// A unit of time, represented as an atomic 64-bit float. -/// -/// According to https://stackoverflow.com/a/873367, as per IEEE754, -/// every integer between 1 and 2^53 can be represented exactly. -/// This should mean that, even at 192kHz sampling rate, over 1 year of audio -/// can be clocked in microseconds with f64 without losing precision. -pub trait TimeUnit { - /// Returns current value - fn get (&self) -> f64; - /// Sets new value, returns old - fn set (&self, value: f64) -> f64; -} -/// Implement arithmetic for a unit of time -macro_rules! impl_op { - ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { - let $a = self.get(); let $b = other.get(); Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { - let $a = self.get(); let $b = other as f64; Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { - let $a = self.get(); let $b = other; Self($impl.into()) - } - } - } -} -/// Define and implement a unit of time -macro_rules! impl_time_unit { - ($T:ident) => { - impl TimeUnit for $T { - fn get (&self) -> f64 { self.0.load(Ordering::Relaxed) } - fn set (&self, value: f64) -> f64 { - let old = self.get(); - self.0.store(value, Ordering::Relaxed); - old - } - } - impl_op!($T, Add, add, |a, b|{a + b}); - impl_op!($T, Sub, sub, |a, b|{a - b}); - impl_op!($T, Mul, mul, |a, b|{a * b}); - impl_op!($T, Div, div, |a, b|{a / b}); - impl_op!($T, Rem, rem, |a, b|{a % b}); - impl From for $T { fn from (value: f64) -> Self { Self(value.into()) } } - impl From for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } } - impl Into for $T { fn into (self) -> f64 { self.get() } } - impl Into for $T { fn into (self) -> usize { self.get() as usize } } - impl Into for &$T { fn into (self) -> f64 { self.get() } } - impl Into for &$T { fn into (self) -> usize { self.get() as usize } } - impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } - } -} - -/// Audio sample rate in Hz (samples per second) -#[derive(Debug, Default)] pub struct SampleRate(AtomicF64); -impl_time_unit!(SampleRate); -impl SampleRate { - /// Return the duration of a sample in microseconds (floating) - #[inline] pub fn usec_per_sample (&self) -> f64 { - 1_000_000f64 / self.get() - } - /// Return the duration of a sample in microseconds (floating) - #[inline] pub fn sample_per_usec (&self) -> f64 { - self.get() / 1_000_000f64 - } - /// Convert a number of samples to microseconds (floating) - #[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 { - self.usec_per_sample() * samples - } - /// Convert a number of microseconds to samples (floating) - #[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 { - self.sample_per_usec() * usecs - } -} - -/// Tempo in beats per minute -#[derive(Debug, Default)] pub struct BeatsPerMinute(AtomicF64); -impl_time_unit!(BeatsPerMinute); - -/// MIDI resolution in PPQ (pulses per quarter note) -#[derive(Debug, Default)] pub struct PulsesPerQuaver(AtomicF64); -impl_time_unit!(PulsesPerQuaver); - -/// Timestamp in microseconds -#[derive(Debug, Default)] pub struct Microsecond(AtomicF64); -impl_time_unit!(Microsecond); -impl Microsecond { - #[inline] pub fn format_msu (&self) -> String { - let usecs = self.get() as usize; - let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); - let (minutes, seconds) = (seconds / 60, seconds % 60); - format!("{minutes}:{seconds:02}:{msecs:03}") - } -} - -/// Timestamp in audio samples -#[derive(Debug, Default)] pub struct SampleCount(AtomicF64); -impl_time_unit!(SampleCount); - -/// Timestamp in MIDI pulses -#[derive(Debug, Default)] pub struct Pulse(AtomicF64); -impl_time_unit!(Pulse); - -/// Quantization setting for launching clips -#[derive(Debug, Default)] pub struct LaunchSync(AtomicF64); -impl_time_unit!(LaunchSync); -impl LaunchSync { - pub fn next (&self) -> f64 { - next_note_length(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - prev_note_length(self.get() as usize) as f64 - } -} - -/// Quantization setting for notes -#[derive(Debug, Default)] pub struct Quantize(AtomicF64); -impl_time_unit!(Quantize); -impl Quantize { - pub fn next (&self) -> f64 { - next_note_length(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - prev_note_length(self.get() as usize) as f64 - } -} - -/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) -#[derive(Debug, Clone)] -pub struct Timebase { - /// Audio samples per second - pub sr: SampleRate, - /// MIDI beats per minute - pub bpm: BeatsPerMinute, - /// MIDI ticks per beat - pub ppq: PulsesPerQuaver, -} -impl Timebase { - /// Specify sample rate, BPM and PPQ - pub fn new ( - s: impl Into, - b: impl Into, - p: impl Into - ) -> Self { - Self { sr: s.into(), bpm: b.into(), ppq: p.into() } - } - /// Iterate over ticks between start and end. - #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator { - TicksIterator { spp: self.samples_per_pulse(), sample: start, start, end } - } - /// Return the duration fo a beat in microseconds - #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } - /// Return the number of beats in a second - #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } - /// Return the number of microseconds corresponding to a note of the given duration - #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { - 4.0 * self.usec_per_beat() * num / den - } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } - /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) - #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } - /// Return number of pulses in a second (BPM-dependent) - #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } - /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) - #[inline] pub fn pulses_per_sample (&self) -> f64 { - self.usec_per_pulse() / self.sr.usec_per_sample() - } - /// Return number of samples in a pulse (SR- and BPM-dependent) - #[inline] pub fn samples_per_pulse (&self) -> f64 { - self.sr.get() / self.pulses_per_second() - } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { - self.pulses_per_sample() * p - } - /// Convert a number of samples to a pulse number (SR- and BPM-dependent) - #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { - s / self.pulses_per_sample() - } - /// Return the number of samples corresponding to a note of the given duration - #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { - self.usec_to_sample(self.note_to_usec(note)) - } - /// Return the number of samples corresponding to the given number of microseconds - #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { - usec * self.sr.get() / 1000f64 - } - /// Return the quantized position of a moment in time given a step - #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { - let step = self.note_to_usec(step); - (time / step, time % step) - } - /// Quantize a collection of events - #[inline] pub fn quantize_into + Sized, T> ( - &self, step: (f64, f64), events: E - ) -> Vec<(f64, f64)> { - events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 0 - #[inline] pub fn format_beats_0 (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - format!("{}.{}.{pulses:02}", beats / 4, beats % 4) - } - /// Format a number of pulses into Beat.Bar starting from 0 - #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4, beats % 4) - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1 (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - format!("{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4 + 1, beats % 4 + 1) - } -} -impl Default for Timebase { - fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) } -} - -#[derive(Debug, Clone)] -pub enum Moment2 { - None, - Zero, - Usec(Microsecond), - Sample(SampleCount), - Pulse(Pulse), -} - -/// A point in time in all time scales (microsecond, sample, MIDI pulse) -#[derive(Debug, Default, Clone)] -pub struct Moment { - pub timebase: Arc, - /// Current time in microseconds - pub usec: Microsecond, - /// Current time in audio samples - pub sample: SampleCount, - /// Current time in MIDI pulses - pub pulse: Pulse, -} -impl Moment { - pub fn zero (timebase: &Arc) -> Self { - Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } - } - pub fn from_usec (timebase: &Arc, usec: f64) -> Self { - Self { - usec: usec.into(), - sample: timebase.sr.usecs_to_sample(usec).into(), - pulse: timebase.usecs_to_pulse(usec).into(), - timebase: timebase.clone(), - } - } - pub fn from_sample (timebase: &Arc, sample: f64) -> Self { - Self { - sample: sample.into(), - usec: timebase.sr.samples_to_usec(sample).into(), - pulse: timebase.samples_to_pulse(sample).into(), - timebase: timebase.clone(), - } - } - pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { - Self { - pulse: pulse.into(), - sample: timebase.pulses_to_sample(pulse).into(), - usec: timebase.pulses_to_usec(pulse).into(), - timebase: timebase.clone(), - } - } - #[inline] pub fn update_from_usec (&self, usec: f64) { - self.usec.set(usec); - self.pulse.set(self.timebase.usecs_to_pulse(usec)); - self.sample.set(self.timebase.sr.usecs_to_sample(usec)); - } - #[inline] pub fn update_from_sample (&self, sample: f64) { - self.usec.set(self.timebase.sr.samples_to_usec(sample)); - self.pulse.set(self.timebase.samples_to_pulse(sample)); - self.sample.set(sample); - } - #[inline] pub fn update_from_pulse (&self, pulse: f64) { - self.usec.set(self.timebase.pulses_to_usec(pulse)); - self.pulse.set(pulse); - self.sample.set(self.timebase.pulses_to_sample(pulse)); - } - #[inline] pub fn format_beat (&self) -> String { - self.timebase.format_beats_1(self.pulse.get()) - } -} -/// Iterator that emits subsequent ticks within a range. -pub struct TicksIterator { - spp: f64, - sample: usize, - start: usize, - end: usize, -} -impl Iterator for TicksIterator { - type Item = (usize, usize); - fn next (&mut self) -> Option { - loop { - if self.sample > self.end { return None } - let spp = self.spp; - let sample = self.sample as f64; - let start = self.start; - let end = self.end; - self.sample += 1; - //println!("{spp} {sample} {start} {end}"); - let jitter = sample.rem_euclid(spp); // ramps - let next_jitter = (sample + 1.0).rem_euclid(spp); - if jitter > next_jitter { // at crossing: - let time = (sample as usize) % (end as usize-start as usize); - let tick = (sample / spp) as usize; - return Some((time, tick)) - } - } - } -} +pub(crate) mod bpm; pub(crate) use bpm::*; +pub(crate) mod clock; pub(crate) use clock::*; +pub(crate) mod moment; pub(crate) use moment::*; +pub(crate) mod perf; pub(crate) use perf::*; +pub(crate) mod ppq; pub(crate) use ppq::*; +pub(crate) mod quant; pub(crate) use quant::*; +pub(crate) mod sr; pub(crate) use sr::*; +pub(crate) mod timebase; pub(crate) use timebase::*; +pub(crate) mod unit; pub(crate) use unit::*; +pub(crate) mod usec; pub(crate) use usec::*; /// (pulses, name), assuming 96 PPQ pub const NOTE_DURATIONS: [(usize, &str);26] = [ diff --git a/crates/tek/src/time/bpm.rs b/crates/tek/src/time/bpm.rs new file mode 100644 index 00000000..30fffb9d --- /dev/null +++ b/crates/tek/src/time/bpm.rs @@ -0,0 +1,5 @@ +use crate::*; + +/// Tempo in beats per minute +#[derive(Debug, Default)] pub struct BeatsPerMinute(AtomicF64); +impl_time_unit!(BeatsPerMinute); diff --git a/crates/tek/src/time/moment.rs b/crates/tek/src/time/moment.rs new file mode 100644 index 00000000..aafaef6a --- /dev/null +++ b/crates/tek/src/time/moment.rs @@ -0,0 +1,69 @@ +use crate::*; + +#[derive(Debug, Clone)] +pub enum Moment2 { + None, + Zero, + Usec(Microsecond), + Sample(SampleCount), + Pulse(Pulse), +} + +/// A point in time in all time scales (microsecond, sample, MIDI pulse) +#[derive(Debug, Default, Clone)] +pub struct Moment { + pub timebase: Arc, + /// Current time in microseconds + pub usec: Microsecond, + /// Current time in audio samples + pub sample: SampleCount, + /// Current time in MIDI pulses + pub pulse: Pulse, +} +impl Moment { + pub fn zero (timebase: &Arc) -> Self { + Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } + } + pub fn from_usec (timebase: &Arc, usec: f64) -> Self { + Self { + usec: usec.into(), + sample: timebase.sr.usecs_to_sample(usec).into(), + pulse: timebase.usecs_to_pulse(usec).into(), + timebase: timebase.clone(), + } + } + pub fn from_sample (timebase: &Arc, sample: f64) -> Self { + Self { + sample: sample.into(), + usec: timebase.sr.samples_to_usec(sample).into(), + pulse: timebase.samples_to_pulse(sample).into(), + timebase: timebase.clone(), + } + } + pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { + Self { + pulse: pulse.into(), + sample: timebase.pulses_to_sample(pulse).into(), + usec: timebase.pulses_to_usec(pulse).into(), + timebase: timebase.clone(), + } + } + #[inline] pub fn update_from_usec (&self, usec: f64) { + self.usec.set(usec); + self.pulse.set(self.timebase.usecs_to_pulse(usec)); + self.sample.set(self.timebase.sr.usecs_to_sample(usec)); + } + #[inline] pub fn update_from_sample (&self, sample: f64) { + self.usec.set(self.timebase.sr.samples_to_usec(sample)); + self.pulse.set(self.timebase.samples_to_pulse(sample)); + self.sample.set(sample); + } + #[inline] pub fn update_from_pulse (&self, pulse: f64) { + self.usec.set(self.timebase.pulses_to_usec(pulse)); + self.pulse.set(pulse); + self.sample.set(self.timebase.pulses_to_sample(pulse)); + } + #[inline] pub fn format_beat (&self) -> String { + self.timebase.format_beats_1(self.pulse.get()) + } +} diff --git a/crates/tek/src/time/ppq.rs b/crates/tek/src/time/ppq.rs new file mode 100644 index 00000000..964b36eb --- /dev/null +++ b/crates/tek/src/time/ppq.rs @@ -0,0 +1,42 @@ +use crate::*; + +pub const DEFAULT_PPQ: f64 = 96.0; +/// FIXME: remove this and use PPQ from timebase everywhere: +pub const PPQ: usize = 96; + +/// MIDI resolution in PPQ (pulses per quarter note) +#[derive(Debug, Default)] pub struct PulsesPerQuaver(AtomicF64); +impl_time_unit!(PulsesPerQuaver); + +/// Timestamp in MIDI pulses +#[derive(Debug, Default)] pub struct Pulse(AtomicF64); +impl_time_unit!(Pulse); + +/// Iterator that emits subsequent ticks within a range. +pub struct TicksIterator { + pub spp: f64, + pub sample: usize, + pub start: usize, + pub end: usize, +} +impl Iterator for TicksIterator { + type Item = (usize, usize); + fn next (&mut self) -> Option { + loop { + if self.sample > self.end { return None } + let spp = self.spp; + let sample = self.sample as f64; + let start = self.start; + let end = self.end; + self.sample += 1; + //println!("{spp} {sample} {start} {end}"); + let jitter = sample.rem_euclid(spp); // ramps + let next_jitter = (sample + 1.0).rem_euclid(spp); + if jitter > next_jitter { // at crossing: + let time = (sample as usize) % (end as usize-start as usize); + let tick = (sample / spp) as usize; + return Some((time, tick)) + } + } + } +} diff --git a/crates/tek/src/time/quant.rs b/crates/tek/src/time/quant.rs new file mode 100644 index 00000000..dc05e271 --- /dev/null +++ b/crates/tek/src/time/quant.rs @@ -0,0 +1,25 @@ +use crate::*; + +/// Quantization setting for launching clips +#[derive(Debug, Default)] pub struct LaunchSync(AtomicF64); +impl_time_unit!(LaunchSync); +impl LaunchSync { + pub fn next (&self) -> f64 { + next_note_length(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + prev_note_length(self.get() as usize) as f64 + } +} + +/// Quantization setting for notes +#[derive(Debug, Default)] pub struct Quantize(AtomicF64); +impl_time_unit!(Quantize); +impl Quantize { + pub fn next (&self) -> f64 { + next_note_length(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + prev_note_length(self.get() as usize) as f64 + } +} diff --git a/crates/tek/src/time/sr.rs b/crates/tek/src/time/sr.rs new file mode 100644 index 00000000..1d3edd75 --- /dev/null +++ b/crates/tek/src/time/sr.rs @@ -0,0 +1,27 @@ +use crate::*; + +/// Audio sample rate in Hz (samples per second) +#[derive(Debug, Default)] pub struct SampleRate(AtomicF64); +impl_time_unit!(SampleRate); +impl SampleRate { + /// Return the duration of a sample in microseconds (floating) + #[inline] pub fn usec_per_sample (&self) -> f64 { + 1_000_000f64 / self.get() + } + /// Return the duration of a sample in microseconds (floating) + #[inline] pub fn sample_per_usec (&self) -> f64 { + self.get() / 1_000_000f64 + } + /// Convert a number of samples to microseconds (floating) + #[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 { + self.usec_per_sample() * samples + } + /// Convert a number of microseconds to samples (floating) + #[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 { + self.sample_per_usec() * usecs + } +} + +/// Timestamp in audio samples +#[derive(Debug, Default)] pub struct SampleCount(AtomicF64); +impl_time_unit!(SampleCount); diff --git a/crates/tek/src/time/timebase.rs b/crates/tek/src/time/timebase.rs new file mode 100644 index 00000000..cc898b92 --- /dev/null +++ b/crates/tek/src/time/timebase.rs @@ -0,0 +1,112 @@ +use crate::*; + +/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) +#[derive(Debug, Clone)] +pub struct Timebase { + /// Audio samples per second + pub sr: SampleRate, + /// MIDI beats per minute + pub bpm: BeatsPerMinute, + /// MIDI ticks per beat + pub ppq: PulsesPerQuaver, +} + +impl Timebase { + /// Specify sample rate, BPM and PPQ + pub fn new ( + s: impl Into, + b: impl Into, + p: impl Into + ) -> Self { + Self { sr: s.into(), bpm: b.into(), ppq: p.into() } + } + /// Iterate over ticks between start and end. + #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator { + TicksIterator { spp: self.samples_per_pulse(), sample: start, start, end } + } + /// Return the duration fo a beat in microseconds + #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } + /// Return the number of beats in a second + #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } + /// Return the number of microseconds corresponding to a note of the given duration + #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { + 4.0 * self.usec_per_beat() * num / den + } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } + /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) + #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } + /// Return number of pulses in a second (BPM-dependent) + #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } + /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) + #[inline] pub fn pulses_per_sample (&self) -> f64 { + self.usec_per_pulse() / self.sr.usec_per_sample() + } + /// Return number of samples in a pulse (SR- and BPM-dependent) + #[inline] pub fn samples_per_pulse (&self) -> f64 { + self.sr.get() / self.pulses_per_second() + } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { + self.pulses_per_sample() * p + } + /// Convert a number of samples to a pulse number (SR- and BPM-dependent) + #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { + s / self.pulses_per_sample() + } + /// Return the number of samples corresponding to a note of the given duration + #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { + self.usec_to_sample(self.note_to_usec(note)) + } + /// Return the number of samples corresponding to the given number of microseconds + #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { + usec * self.sr.get() / 1000f64 + } + /// Return the quantized position of a moment in time given a step + #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { + let step = self.note_to_usec(step); + (time / step, time % step) + } + /// Quantize a collection of events + #[inline] pub fn quantize_into + Sized, T> ( + &self, step: (f64, f64), events: E + ) -> Vec<(f64, f64)> { + events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 0 + #[inline] pub fn format_beats_0 (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + format!("{}.{}.{pulses:02}", beats / 4, beats % 4) + } + /// Format a number of pulses into Beat.Bar starting from 0 + #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4, beats % 4) + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1 (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + format!("{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4 + 1, beats % 4 + 1) + } +} + +impl Default for Timebase { + fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) } +} diff --git a/crates/tek/src/time/unit.rs b/crates/tek/src/time/unit.rs new file mode 100644 index 00000000..a0ad30e7 --- /dev/null +++ b/crates/tek/src/time/unit.rs @@ -0,0 +1,61 @@ +use crate::*; + +/// A unit of time, represented as an atomic 64-bit float. +/// +/// According to https://stackoverflow.com/a/873367, as per IEEE754, +/// every integer between 1 and 2^53 can be represented exactly. +/// This should mean that, even at 192kHz sampling rate, over 1 year of audio +/// can be clocked in microseconds with f64 without losing precision. +pub trait TimeUnit { + /// Returns current value + fn get (&self) -> f64; + /// Sets new value, returns old + fn set (&self, value: f64) -> f64; +} + +/// Implement arithmetic for a unit of time +#[macro_export] macro_rules! impl_op { + ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { + let $a = self.get(); let $b = other.get(); Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { + let $a = self.get(); let $b = other as f64; Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { + let $a = self.get(); let $b = other; Self($impl.into()) + } + } + } +} + +/// Define and implement a unit of time +#[macro_export] macro_rules! impl_time_unit { + ($T:ident) => { + impl TimeUnit for $T { + fn get (&self) -> f64 { self.0.load(Ordering::Relaxed) } + fn set (&self, value: f64) -> f64 { + let old = self.get(); + self.0.store(value, Ordering::Relaxed); + old + } + } + impl_op!($T, Add, add, |a, b|{a + b}); + impl_op!($T, Sub, sub, |a, b|{a - b}); + impl_op!($T, Mul, mul, |a, b|{a * b}); + impl_op!($T, Div, div, |a, b|{a / b}); + impl_op!($T, Rem, rem, |a, b|{a % b}); + impl From for $T { fn from (value: f64) -> Self { Self(value.into()) } } + impl From for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } } + impl Into for $T { fn into (self) -> f64 { self.get() } } + impl Into for $T { fn into (self) -> usize { self.get() as usize } } + impl Into for &$T { fn into (self) -> f64 { self.get() } } + impl Into for &$T { fn into (self) -> usize { self.get() as usize } } + impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } + } +} diff --git a/crates/tek/src/time/usec.rs b/crates/tek/src/time/usec.rs new file mode 100644 index 00000000..8f768861 --- /dev/null +++ b/crates/tek/src/time/usec.rs @@ -0,0 +1,13 @@ +use crate::*; + +/// Timestamp in microseconds +#[derive(Debug, Default)] pub struct Microsecond(AtomicF64); +impl_time_unit!(Microsecond); +impl Microsecond { + #[inline] pub fn format_msu (&self) -> String { + let usecs = self.get() as usize; + let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); + let (minutes, seconds) = (seconds / 60, seconds % 60); + format!("{minutes}:{seconds:02}:{msecs:03}") + } +} From 3c990e9f63a25c849cb8642e02153e48011c42b1 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 16:10:42 +0100 Subject: [PATCH 024/905] merge layout/ with space/ --- crates/tek/src/layout.rs | 20 ---------------- crates/tek/src/lib.rs | 1 - crates/tek/src/space.rs | 23 +++++++++++++++++++ crates/tek/src/{layout => space}/align.rs | 0 crates/tek/src/{layout => space}/bsp.rs | 0 crates/tek/src/{layout => space}/collect.rs | 0 crates/tek/src/{layout => space}/cond.rs | 0 crates/tek/src/{layout => space}/fill.rs | 0 crates/tek/src/{layout => space}/fixed.rs | 0 .../tek/src/{layout => space}/inset_outset.rs | 0 crates/tek/src/{layout => space}/layers.rs | 0 .../tek/src/{layout => space}/map_reduce.rs | 0 crates/tek/src/{layout => space}/measure.rs | 0 crates/tek/src/{layout => space}/min_max.rs | 0 crates/tek/src/{layout => space}/push_pull.rs | 0 crates/tek/src/{layout => space}/scroll.rs | 0 .../tek/src/{layout => space}/shrink_grow.rs | 0 crates/tek/src/{layout => space}/split.rs | 0 crates/tek/src/{layout => space}/stack.rs | 0 19 files changed, 23 insertions(+), 21 deletions(-) delete mode 100644 crates/tek/src/layout.rs rename crates/tek/src/{layout => space}/align.rs (100%) rename crates/tek/src/{layout => space}/bsp.rs (100%) rename crates/tek/src/{layout => space}/collect.rs (100%) rename crates/tek/src/{layout => space}/cond.rs (100%) rename crates/tek/src/{layout => space}/fill.rs (100%) rename crates/tek/src/{layout => space}/fixed.rs (100%) rename crates/tek/src/{layout => space}/inset_outset.rs (100%) rename crates/tek/src/{layout => space}/layers.rs (100%) rename crates/tek/src/{layout => space}/map_reduce.rs (100%) rename crates/tek/src/{layout => space}/measure.rs (100%) rename crates/tek/src/{layout => space}/min_max.rs (100%) rename crates/tek/src/{layout => space}/push_pull.rs (100%) rename crates/tek/src/{layout => space}/scroll.rs (100%) rename crates/tek/src/{layout => space}/shrink_grow.rs (100%) rename crates/tek/src/{layout => space}/split.rs (100%) rename crates/tek/src/{layout => space}/stack.rs (100%) diff --git a/crates/tek/src/layout.rs b/crates/tek/src/layout.rs deleted file mode 100644 index 89f4db97..00000000 --- a/crates/tek/src/layout.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub(crate) mod align; pub(crate) use align::*; -pub(crate) mod bsp; pub(crate) use bsp::*; -pub(crate) mod cond; pub(crate) use cond::*; -pub(crate) mod fill; pub(crate) use fill::*; -pub(crate) mod fixed; pub(crate) use fixed::*; -pub(crate) mod inset_outset; pub(crate) use inset_outset::*; -pub(crate) mod layers; pub(crate) use layers::*; -pub(crate) mod measure; pub(crate) use measure::*; -pub(crate) mod min_max; pub(crate) use min_max::*; -pub(crate) mod push_pull; pub(crate) use push_pull::*; -pub(crate) mod scroll; pub(crate) use scroll::*; -pub(crate) mod shrink_grow; pub(crate) use shrink_grow::*; -pub(crate) mod split; pub(crate) use split::*; -pub(crate) mod stack; pub(crate) use stack::*; - -pub use self::{ - align::*, - bsp::*, - fill::*, -}; diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs index f63205e5..73455b36 100644 --- a/crates/tek/src/lib.rs +++ b/crates/tek/src/lib.rs @@ -1,5 +1,4 @@ pub mod core; pub(crate) use self::core::*; -pub mod layout; pub(crate) use self::layout::*; pub mod api; pub(crate) use self::api::*; pub mod tui; pub(crate) use self::tui::*; pub mod edn; pub(crate) use self::edn::*; diff --git a/crates/tek/src/space.rs b/crates/tek/src/space.rs index bb2ca859..bdad4973 100644 --- a/crates/tek/src/space.rs +++ b/crates/tek/src/space.rs @@ -5,3 +5,26 @@ pub(crate) mod direction; pub(crate) use direction::*; // TODO: return impl Point and impl Size instead of [N;x] // to disambiguate between usage of 2-"tuple"s + +////////////////////////////////////////////////////// + +pub(crate) mod align; pub(crate) use align::*; +pub(crate) mod bsp; pub(crate) use bsp::*; +pub(crate) mod cond; pub(crate) use cond::*; +pub(crate) mod fill; pub(crate) use fill::*; +pub(crate) mod fixed; pub(crate) use fixed::*; +pub(crate) mod inset_outset; pub(crate) use inset_outset::*; +pub(crate) mod layers; pub(crate) use layers::*; +pub(crate) mod measure; pub(crate) use measure::*; +pub(crate) mod min_max; pub(crate) use min_max::*; +pub(crate) mod push_pull; pub(crate) use push_pull::*; +pub(crate) mod scroll; pub(crate) use scroll::*; +pub(crate) mod shrink_grow; pub(crate) use shrink_grow::*; +pub(crate) mod split; pub(crate) use split::*; +pub(crate) mod stack; pub(crate) use stack::*; + +pub use self::{ + align::*, + bsp::*, + fill::*, +}; diff --git a/crates/tek/src/layout/align.rs b/crates/tek/src/space/align.rs similarity index 100% rename from crates/tek/src/layout/align.rs rename to crates/tek/src/space/align.rs diff --git a/crates/tek/src/layout/bsp.rs b/crates/tek/src/space/bsp.rs similarity index 100% rename from crates/tek/src/layout/bsp.rs rename to crates/tek/src/space/bsp.rs diff --git a/crates/tek/src/layout/collect.rs b/crates/tek/src/space/collect.rs similarity index 100% rename from crates/tek/src/layout/collect.rs rename to crates/tek/src/space/collect.rs diff --git a/crates/tek/src/layout/cond.rs b/crates/tek/src/space/cond.rs similarity index 100% rename from crates/tek/src/layout/cond.rs rename to crates/tek/src/space/cond.rs diff --git a/crates/tek/src/layout/fill.rs b/crates/tek/src/space/fill.rs similarity index 100% rename from crates/tek/src/layout/fill.rs rename to crates/tek/src/space/fill.rs diff --git a/crates/tek/src/layout/fixed.rs b/crates/tek/src/space/fixed.rs similarity index 100% rename from crates/tek/src/layout/fixed.rs rename to crates/tek/src/space/fixed.rs diff --git a/crates/tek/src/layout/inset_outset.rs b/crates/tek/src/space/inset_outset.rs similarity index 100% rename from crates/tek/src/layout/inset_outset.rs rename to crates/tek/src/space/inset_outset.rs diff --git a/crates/tek/src/layout/layers.rs b/crates/tek/src/space/layers.rs similarity index 100% rename from crates/tek/src/layout/layers.rs rename to crates/tek/src/space/layers.rs diff --git a/crates/tek/src/layout/map_reduce.rs b/crates/tek/src/space/map_reduce.rs similarity index 100% rename from crates/tek/src/layout/map_reduce.rs rename to crates/tek/src/space/map_reduce.rs diff --git a/crates/tek/src/layout/measure.rs b/crates/tek/src/space/measure.rs similarity index 100% rename from crates/tek/src/layout/measure.rs rename to crates/tek/src/space/measure.rs diff --git a/crates/tek/src/layout/min_max.rs b/crates/tek/src/space/min_max.rs similarity index 100% rename from crates/tek/src/layout/min_max.rs rename to crates/tek/src/space/min_max.rs diff --git a/crates/tek/src/layout/push_pull.rs b/crates/tek/src/space/push_pull.rs similarity index 100% rename from crates/tek/src/layout/push_pull.rs rename to crates/tek/src/space/push_pull.rs diff --git a/crates/tek/src/layout/scroll.rs b/crates/tek/src/space/scroll.rs similarity index 100% rename from crates/tek/src/layout/scroll.rs rename to crates/tek/src/space/scroll.rs diff --git a/crates/tek/src/layout/shrink_grow.rs b/crates/tek/src/space/shrink_grow.rs similarity index 100% rename from crates/tek/src/layout/shrink_grow.rs rename to crates/tek/src/space/shrink_grow.rs diff --git a/crates/tek/src/layout/split.rs b/crates/tek/src/space/split.rs similarity index 100% rename from crates/tek/src/layout/split.rs rename to crates/tek/src/space/split.rs diff --git a/crates/tek/src/layout/stack.rs b/crates/tek/src/space/stack.rs similarity index 100% rename from crates/tek/src/layout/stack.rs rename to crates/tek/src/space/stack.rs From 3d669d7d24980103ded9bb87c15aa31a7d4bfb3a Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 16:23:48 +0100 Subject: [PATCH 025/905] readd todos --- crates/tek/src/api.rs | 3 - crates/tek/src/api/_todo_api_plugin_kind.rs | 21 -- crates/tek/src/audio.rs | 3 + crates/tek/src/audio/audio_in.rs | 0 crates/tek/src/audio/audio_out.rs | 0 .../_todo_api_channel.rs => audio/channel.rs} | 0 .../_todo_api_mixer.rs => audio/mixer.rs} | 0 crates/tek/src/{api => audio}/sampler.rs | 0 crates/tek/src/jack.rs | 12 +- crates/tek/src/lib.rs | 15 +- crates/tek/src/midi.rs | 262 +++++++++++++++++- .../{api/phrase.rs => midi/midi_phrase.rs} | 0 crates/tek/src/midi/midi_player.rs | 255 ----------------- .../src/{api/scene.rs => midi/midi_scene.rs} | 0 .../src/{api/track.rs => midi/midi_track.rs} | 0 .../{api/_todo_api_plugin.rs => plugin.rs} | 23 +- .../_todo_api_plugin_lv2.rs => plugin/lv2.rs} | 0 crates/tek/src/tui/app_arranger.rs | 2 - crates/tek/src/tui/phrase_list.rs | 2 +- 19 files changed, 299 insertions(+), 299 deletions(-) delete mode 100644 crates/tek/src/api/_todo_api_plugin_kind.rs create mode 100644 crates/tek/src/audio.rs create mode 100644 crates/tek/src/audio/audio_in.rs create mode 100644 crates/tek/src/audio/audio_out.rs rename crates/tek/src/{api/_todo_api_channel.rs => audio/channel.rs} (100%) rename crates/tek/src/{api/_todo_api_mixer.rs => audio/mixer.rs} (100%) rename crates/tek/src/{api => audio}/sampler.rs (100%) rename crates/tek/src/{api/phrase.rs => midi/midi_phrase.rs} (100%) delete mode 100644 crates/tek/src/midi/midi_player.rs rename crates/tek/src/{api/scene.rs => midi/midi_scene.rs} (100%) rename crates/tek/src/{api/track.rs => midi/midi_track.rs} (100%) rename crates/tek/src/{api/_todo_api_plugin.rs => plugin.rs} (86%) rename crates/tek/src/{api/_todo_api_plugin_lv2.rs => plugin/lv2.rs} (100%) diff --git a/crates/tek/src/api.rs b/crates/tek/src/api.rs index cc13d65c..a4b2c74b 100644 --- a/crates/tek/src/api.rs +++ b/crates/tek/src/api.rs @@ -1,4 +1 @@ -mod phrase; pub(crate) use phrase::*; -mod scene; pub(crate) use scene::*; -mod track; pub(crate) use track::*; mod sampler; pub(crate) use sampler::*; diff --git a/crates/tek/src/api/_todo_api_plugin_kind.rs b/crates/tek/src/api/_todo_api_plugin_kind.rs deleted file mode 100644 index 0f35ca3a..00000000 --- a/crates/tek/src/api/_todo_api_plugin_kind.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::*; - -/// Supported plugin formats. -#[derive(Default)] -pub enum PluginKind { - #[default] None, - LV2(LV2Plugin), - VST2 { instance: ::vst::host::PluginInstance }, - VST3, -} - -impl Debug for PluginKind { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> { - write!(f, "{}", match self { - Self::None => "(none)", - Self::LV2(_) => "LV2", - Self::VST2{..} => "VST2", - Self::VST3 => "VST3", - }) - } -} diff --git a/crates/tek/src/audio.rs b/crates/tek/src/audio.rs new file mode 100644 index 00000000..46aa0557 --- /dev/null +++ b/crates/tek/src/audio.rs @@ -0,0 +1,3 @@ +pub(crate) mod audio_in; pub(crate) use audio_in::*; +pub(crate) mod audio_out; pub(crate) use audio_out::*; +pub(crate) mod sampler; pub(crate) use sampler::*; diff --git a/crates/tek/src/audio/audio_in.rs b/crates/tek/src/audio/audio_in.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/tek/src/audio/audio_out.rs b/crates/tek/src/audio/audio_out.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/tek/src/api/_todo_api_channel.rs b/crates/tek/src/audio/channel.rs similarity index 100% rename from crates/tek/src/api/_todo_api_channel.rs rename to crates/tek/src/audio/channel.rs diff --git a/crates/tek/src/api/_todo_api_mixer.rs b/crates/tek/src/audio/mixer.rs similarity index 100% rename from crates/tek/src/api/_todo_api_mixer.rs rename to crates/tek/src/audio/mixer.rs diff --git a/crates/tek/src/api/sampler.rs b/crates/tek/src/audio/sampler.rs similarity index 100% rename from crates/tek/src/api/sampler.rs rename to crates/tek/src/audio/sampler.rs diff --git a/crates/tek/src/jack.rs b/crates/tek/src/jack.rs index ac04ed72..aabea352 100644 --- a/crates/tek/src/jack.rs +++ b/crates/tek/src/jack.rs @@ -1,11 +1,11 @@ use crate::*; -pub(crate) mod activate; pub(crate) use activate::*; -pub(crate) mod audio; pub(crate) use audio::*; -pub(crate) mod client; pub(crate) use client::*; -pub(crate) mod from_jack; pub(crate) use from_jack::*; -pub(crate) mod jack_event; pub(crate) use jack_event::*; -pub(crate) mod ports; pub(crate) use ports::*; +pub(crate) mod activate; pub(crate) use self::activate::*; +pub(crate) mod audio; pub(crate) use self::audio::*; +pub(crate) mod client; pub(crate) use self::client::*; +pub(crate) mod from_jack; pub(crate) use self::from_jack::*; +pub(crate) mod jack_event; pub(crate) use self::jack_event::*; +pub(crate) mod ports; pub(crate) use self::ports::*; //////////////////////////////////////////////////////////////////////////////////// diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs index 73455b36..782eac2e 100644 --- a/crates/tek/src/lib.rs +++ b/crates/tek/src/lib.rs @@ -1,11 +1,12 @@ pub mod core; pub(crate) use self::core::*; -pub mod api; pub(crate) use self::api::*; -pub mod tui; pub(crate) use self::tui::*; -pub mod edn; pub(crate) use self::edn::*; -pub mod jack; pub(crate) use self::jack::*; -pub mod midi; pub(crate) use self::midi::*; -pub mod time; pub(crate) use self::time::*; -pub mod space; pub(crate) use self::space::*; +pub mod time; pub(crate) use self::time::*; +pub mod space; pub(crate) use self::space::*; +pub mod tui; pub(crate) use self::tui::*; +pub mod edn; pub(crate) use self::edn::*; +pub mod jack; pub(crate) use self::jack::*; +pub mod midi; pub(crate) use self::midi::*; +pub mod audio; pub(crate) use self::audio::*; +//pub mod plugin; pub(crate) use self::plugin::*; /// Standard result type. pub type Usually = Result>; diff --git a/crates/tek/src/midi.rs b/crates/tek/src/midi.rs index db92d43f..240d102f 100644 --- a/crates/tek/src/midi.rs +++ b/crates/tek/src/midi.rs @@ -1,12 +1,14 @@ use crate::*; -pub(crate) mod midi_note; pub(crate) use midi_note::*; pub(crate) mod midi_in; pub(crate) use midi_in::*; -pub(crate) mod midi_out; pub(crate) use midi_out::*; -pub(crate) mod midi_player; pub(crate) use midi_player::*; pub(crate) mod midi_launch; pub(crate) use midi_launch::*; +pub(crate) mod midi_note; pub(crate) use midi_note::*; +pub(crate) mod midi_out; pub(crate) use midi_out::*; +pub(crate) mod midi_phrase; pub(crate) use midi_phrase::*; pub(crate) mod midi_play; pub(crate) use midi_play::*; pub(crate) mod midi_rec; pub(crate) use midi_rec::*; +pub(crate) mod midi_scene; pub(crate) use midi_scene::*; +pub(crate) mod midi_track; pub(crate) use midi_track::*; /// Add "all notes off" to the start of a buffer. pub fn all_notes_off (output: &mut [Vec>]) { @@ -55,3 +57,257 @@ pub const MIDI_NOTE_NAMES: [&'static str;128] = [ "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", ]; + +pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {} + +impl MidiPlayerApi for PhrasePlayerModel {} + +pub trait HasPlayer { + fn player (&self) -> &impl MidiPlayerApi; + fn player_mut (&mut self) -> &mut impl MidiPlayerApi; +} + +#[macro_export] macro_rules! has_player { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? { + fn player (&$self) -> &impl MidiPlayerApi { &$cb } + fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb } + } + } +} + +/// Contains state for playing a phrase +pub struct PhrasePlayerModel { + /// State of clock and playhead + pub(crate) clock: ClockModel, + /// Start time and phrase being played + pub(crate) play_phrase: Option<(Moment, Option>>)>, + /// Start time and next phrase + pub(crate) next_phrase: Option<(Moment, Option>>)>, + /// Play input through output. + pub(crate) monitoring: bool, + /// Write input to sequence. + pub(crate) recording: bool, + /// Overdub input to sequence. + pub(crate) overdub: bool, + /// Send all notes off + pub(crate) reset: bool, // TODO?: after Some(nframes) + /// Record from MIDI ports to current sequence. + pub midi_ins: Vec>, + /// Play from current sequence to MIDI ports + pub midi_outs: Vec>, + /// Notes currently held at input + pub(crate) notes_in: Arc>, + /// Notes currently held at output + pub(crate) notes_out: Arc>, + /// MIDI output buffer + pub note_buf: Vec, +} + +impl std::fmt::Debug for PhrasePlayerModel { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("PhrasePlayerModel") + .field("clock", &self.clock) + .field("play_phrase", &self.play_phrase) + .field("next_phrase", &self.next_phrase) + .finish() + } +} + +impl From<&ClockModel> for PhrasePlayerModel { + fn from (clock: &ClockModel) -> Self { + Self { + clock: clock.clone(), + midi_ins: vec![], + midi_outs: vec![], + note_buf: vec![0;8], + reset: true, + recording: false, + monitoring: false, + overdub: false, + play_phrase: None, + next_phrase: None, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + } + } +} + +impl From<(&ClockModel, &Arc>)> for PhrasePlayerModel { + fn from ((clock, phrase): (&ClockModel, &Arc>)) -> Self { + let mut model = Self::from(clock); + model.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone()))); + model + } +} + +has_clock!(|self:PhrasePlayerModel|&self.clock); + +impl HasMidiIns for PhrasePlayerModel { + fn midi_ins (&self) -> &Vec> { + &self.midi_ins + } + fn midi_ins_mut (&mut self) -> &mut Vec> { + &mut self.midi_ins + } +} + +impl HasMidiOuts for PhrasePlayerModel { + fn midi_outs (&self) -> &Vec> { + &self.midi_outs + } + fn midi_outs_mut (&mut self) -> &mut Vec> { + &mut self.midi_outs + } + fn midi_note (&mut self) -> &mut Vec { + &mut self.note_buf + } +} + +/// Hosts the JACK callback for a single MIDI player +pub struct PlayerAudio<'a, T: MidiPlayerApi>( + /// Player + pub &'a mut T, + /// Note buffer + pub &'a mut Vec, + /// Note chunk buffer + pub &'a mut Vec>>, +); + +/// JACK process callback for a sequencer's phrase player/recorder. +impl<'a, T: MidiPlayerApi> Audio for PlayerAudio<'a, T> { + fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + let model = &mut self.0; + let note_buf = &mut self.1; + let midi_buf = &mut self.2; + // Clear output buffer(s) + model.clear(scope, midi_buf, false); + // Write chunk of phrase to output, handle switchover + if model.play(scope, note_buf, midi_buf) { + model.switchover(scope, note_buf, midi_buf); + } + if model.has_midi_ins() { + if model.recording() || model.monitoring() { + // Record and/or monitor input + model.record(scope, midi_buf) + } else if model.has_midi_outs() && model.monitoring() { + // Monitor input to output + model.monitor(scope, midi_buf) + } + } + // Write to output port(s) + model.write(scope, midi_buf); + Control::Continue + } +} + +impl MidiRecordApi for PhrasePlayerModel { + fn recording (&self) -> bool { + self.recording + } + fn recording_mut (&mut self) -> &mut bool { + &mut self.recording + } + fn monitoring (&self) -> bool { + self.monitoring + } + fn monitoring_mut (&mut self) -> &mut bool { + &mut self.monitoring + } + fn overdub (&self) -> bool { + self.overdub + } + fn overdub_mut (&mut self) -> &mut bool { + &mut self.overdub + } + fn notes_in (&self) -> &Arc> { + &self.notes_in + } +} + +impl MidiPlaybackApi for PhrasePlayerModel { + fn notes_out (&self) -> &Arc> { + &self.notes_in + } +} + +impl HasPlayPhrase for PhrasePlayerModel { + fn reset (&self) -> bool { + self.reset + } + fn reset_mut (&mut self) -> &mut bool { + &mut self.reset + } + fn play_phrase (&self) -> &Option<(Moment, Option>>)> { + &self.play_phrase + } + fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.play_phrase + } + fn next_phrase (&self) -> &Option<(Moment, Option>>)> { + &self.next_phrase + } + fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.next_phrase + } +} + +//#[derive(Debug)] +//pub struct MIDIPlayer { + ///// Global timebase + //pub clock: Arc, + ///// Start time and phrase being played + //pub play_phrase: Option<(Moment, Option>>)>, + ///// Start time and next phrase + //pub next_phrase: Option<(Moment, Option>>)>, + ///// Play input through output. + //pub monitoring: bool, + ///// Write input to sequence. + //pub recording: bool, + ///// Overdub input to sequence. + //pub overdub: bool, + ///// Send all notes off + //pub reset: bool, // TODO?: after Some(nframes) + ///// Record from MIDI ports to current sequence. + //pub midi_inputs: Vec>, + ///// Play from current sequence to MIDI ports + //pub midi_outputs: Vec>, + ///// MIDI output buffer + //pub midi_note: Vec, + ///// MIDI output buffer + //pub midi_chunk: Vec>>, + ///// Notes currently held at input + //pub notes_in: Arc>, + ///// Notes currently held at output + //pub notes_out: Arc>, +//} + +///// Methods used primarily by the process callback +//impl MIDIPlayer { + //pub fn new ( + //jack: &Arc>, + //clock: &Arc, + //name: &str + //) -> Usually { + //let jack = jack.read().unwrap(); + //Ok(Self { + //clock: clock.clone(), + //phrase: None, + //next_phrase: None, + //notes_in: Arc::new(RwLock::new([false;128])), + //notes_out: Arc::new(RwLock::new([false;128])), + //monitoring: false, + //recording: false, + //overdub: true, + //reset: true, + //midi_note: Vec::with_capacity(8), + //midi_chunk: vec![Vec::with_capacity(16);16384], + //midi_outputs: vec![ + //jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())? + //], + //midi_inputs: vec![ + //jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())? + //], + //}) + //} +//} diff --git a/crates/tek/src/api/phrase.rs b/crates/tek/src/midi/midi_phrase.rs similarity index 100% rename from crates/tek/src/api/phrase.rs rename to crates/tek/src/midi/midi_phrase.rs diff --git a/crates/tek/src/midi/midi_player.rs b/crates/tek/src/midi/midi_player.rs deleted file mode 100644 index e0eedd3c..00000000 --- a/crates/tek/src/midi/midi_player.rs +++ /dev/null @@ -1,255 +0,0 @@ -use crate::*; - -pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {} - -impl MidiPlayerApi for PhrasePlayerModel {} - -pub trait HasPlayer { - fn player (&self) -> &impl MidiPlayerApi; - fn player_mut (&mut self) -> &mut impl MidiPlayerApi; -} - -#[macro_export] macro_rules! has_player { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? { - fn player (&$self) -> &impl MidiPlayerApi { &$cb } - fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb } - } - } -} - -/// Contains state for playing a phrase -pub struct PhrasePlayerModel { - /// State of clock and playhead - pub(crate) clock: ClockModel, - /// Start time and phrase being played - pub(crate) play_phrase: Option<(Moment, Option>>)>, - /// Start time and next phrase - pub(crate) next_phrase: Option<(Moment, Option>>)>, - /// Play input through output. - pub(crate) monitoring: bool, - /// Write input to sequence. - pub(crate) recording: bool, - /// Overdub input to sequence. - pub(crate) overdub: bool, - /// Send all notes off - pub(crate) reset: bool, // TODO?: after Some(nframes) - /// Record from MIDI ports to current sequence. - pub midi_ins: Vec>, - /// Play from current sequence to MIDI ports - pub midi_outs: Vec>, - /// Notes currently held at input - pub(crate) notes_in: Arc>, - /// Notes currently held at output - pub(crate) notes_out: Arc>, - /// MIDI output buffer - pub note_buf: Vec, -} - -impl std::fmt::Debug for PhrasePlayerModel { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("PhrasePlayerModel") - .field("clock", &self.clock) - .field("play_phrase", &self.play_phrase) - .field("next_phrase", &self.next_phrase) - .finish() - } -} - -impl From<&ClockModel> for PhrasePlayerModel { - fn from (clock: &ClockModel) -> Self { - Self { - clock: clock.clone(), - midi_ins: vec![], - midi_outs: vec![], - note_buf: vec![0;8], - reset: true, - recording: false, - monitoring: false, - overdub: false, - play_phrase: None, - next_phrase: None, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - } - } -} - -impl From<(&ClockModel, &Arc>)> for PhrasePlayerModel { - fn from ((clock, phrase): (&ClockModel, &Arc>)) -> Self { - let mut model = Self::from(clock); - model.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone()))); - model - } -} - -has_clock!(|self:PhrasePlayerModel|&self.clock); - -impl HasMidiIns for PhrasePlayerModel { - fn midi_ins (&self) -> &Vec> { - &self.midi_ins - } - fn midi_ins_mut (&mut self) -> &mut Vec> { - &mut self.midi_ins - } -} - -impl HasMidiOuts for PhrasePlayerModel { - fn midi_outs (&self) -> &Vec> { - &self.midi_outs - } - fn midi_outs_mut (&mut self) -> &mut Vec> { - &mut self.midi_outs - } - fn midi_note (&mut self) -> &mut Vec { - &mut self.note_buf - } -} - -/// Hosts the JACK callback for a single MIDI player -pub struct PlayerAudio<'a, T: MidiPlayerApi>( - /// Player - pub &'a mut T, - /// Note buffer - pub &'a mut Vec, - /// Note chunk buffer - pub &'a mut Vec>>, -); - -/// JACK process callback for a sequencer's phrase player/recorder. -impl<'a, T: MidiPlayerApi> Audio for PlayerAudio<'a, T> { - fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - let model = &mut self.0; - let note_buf = &mut self.1; - let midi_buf = &mut self.2; - // Clear output buffer(s) - model.clear(scope, midi_buf, false); - // Write chunk of phrase to output, handle switchover - if model.play(scope, note_buf, midi_buf) { - model.switchover(scope, note_buf, midi_buf); - } - if model.has_midi_ins() { - if model.recording() || model.monitoring() { - // Record and/or monitor input - model.record(scope, midi_buf) - } else if model.has_midi_outs() && model.monitoring() { - // Monitor input to output - model.monitor(scope, midi_buf) - } - } - // Write to output port(s) - model.write(scope, midi_buf); - Control::Continue - } -} - -impl MidiRecordApi for PhrasePlayerModel { - fn recording (&self) -> bool { - self.recording - } - fn recording_mut (&mut self) -> &mut bool { - &mut self.recording - } - fn monitoring (&self) -> bool { - self.monitoring - } - fn monitoring_mut (&mut self) -> &mut bool { - &mut self.monitoring - } - fn overdub (&self) -> bool { - self.overdub - } - fn overdub_mut (&mut self) -> &mut bool { - &mut self.overdub - } - fn notes_in (&self) -> &Arc> { - &self.notes_in - } -} - -impl MidiPlaybackApi for PhrasePlayerModel { - fn notes_out (&self) -> &Arc> { - &self.notes_in - } -} - -impl HasPlayPhrase for PhrasePlayerModel { - fn reset (&self) -> bool { - self.reset - } - fn reset_mut (&mut self) -> &mut bool { - &mut self.reset - } - fn play_phrase (&self) -> &Option<(Moment, Option>>)> { - &self.play_phrase - } - fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.play_phrase - } - fn next_phrase (&self) -> &Option<(Moment, Option>>)> { - &self.next_phrase - } - fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.next_phrase - } -} - -//#[derive(Debug)] -//pub struct MIDIPlayer { - ///// Global timebase - //pub clock: Arc, - ///// Start time and phrase being played - //pub play_phrase: Option<(Moment, Option>>)>, - ///// Start time and next phrase - //pub next_phrase: Option<(Moment, Option>>)>, - ///// Play input through output. - //pub monitoring: bool, - ///// Write input to sequence. - //pub recording: bool, - ///// Overdub input to sequence. - //pub overdub: bool, - ///// Send all notes off - //pub reset: bool, // TODO?: after Some(nframes) - ///// Record from MIDI ports to current sequence. - //pub midi_inputs: Vec>, - ///// Play from current sequence to MIDI ports - //pub midi_outputs: Vec>, - ///// MIDI output buffer - //pub midi_note: Vec, - ///// MIDI output buffer - //pub midi_chunk: Vec>>, - ///// Notes currently held at input - //pub notes_in: Arc>, - ///// Notes currently held at output - //pub notes_out: Arc>, -//} - -///// Methods used primarily by the process callback -//impl MIDIPlayer { - //pub fn new ( - //jack: &Arc>, - //clock: &Arc, - //name: &str - //) -> Usually { - //let jack = jack.read().unwrap(); - //Ok(Self { - //clock: clock.clone(), - //phrase: None, - //next_phrase: None, - //notes_in: Arc::new(RwLock::new([false;128])), - //notes_out: Arc::new(RwLock::new([false;128])), - //monitoring: false, - //recording: false, - //overdub: true, - //reset: true, - //midi_note: Vec::with_capacity(8), - //midi_chunk: vec![Vec::with_capacity(16);16384], - //midi_outputs: vec![ - //jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())? - //], - //midi_inputs: vec![ - //jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())? - //], - //}) - //} -//} diff --git a/crates/tek/src/api/scene.rs b/crates/tek/src/midi/midi_scene.rs similarity index 100% rename from crates/tek/src/api/scene.rs rename to crates/tek/src/midi/midi_scene.rs diff --git a/crates/tek/src/api/track.rs b/crates/tek/src/midi/midi_track.rs similarity index 100% rename from crates/tek/src/api/track.rs rename to crates/tek/src/midi/midi_track.rs diff --git a/crates/tek/src/api/_todo_api_plugin.rs b/crates/tek/src/plugin.rs similarity index 86% rename from crates/tek/src/api/_todo_api_plugin.rs rename to crates/tek/src/plugin.rs index 161c59d7..086f17bf 100644 --- a/crates/tek/src/api/_todo_api_plugin.rs +++ b/crates/tek/src/plugin.rs @@ -1,3 +1,4 @@ +pub(crate) mod lv2; pub(crate) use lv2::*; use crate::*; /// A plugin device. @@ -15,6 +16,26 @@ pub struct Plugin { pub audio_ins: Vec>, pub audio_outs: Vec>, } + +/// Supported plugin formats. +#[derive(Default)] +pub enum PluginKind { + #[default] None, + LV2(LV2Plugin), + VST2 { instance: ::vst::host::PluginInstance }, + VST3, +} + +impl Debug for PluginKind { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> { + write!(f, "{}", match self { + Self::None => "(none)", + Self::LV2(_) => "LV2", + Self::VST2{..} => "VST2", + Self::VST3 => "VST3", + }) + } +} impl Plugin { pub fn new_lv2 ( jack: &Arc>, @@ -62,7 +83,7 @@ impl From<&Arc>> for PluginAudio { } } -audio!(|self: PluginAudio, _, _|{ +audio!(|self: PluginAudio, client_, _scope|{ let state = &mut*self.0.write().unwrap(); match state.plugin.as_mut() { Some(PluginKind::LV2(LV2Plugin { diff --git a/crates/tek/src/api/_todo_api_plugin_lv2.rs b/crates/tek/src/plugin/lv2.rs similarity index 100% rename from crates/tek/src/api/_todo_api_plugin_lv2.rs rename to crates/tek/src/plugin/lv2.rs diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index e22156fa..7c8920ce 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -1,6 +1,4 @@ use crate::*; -use crate::api::ArrangerTrackCommand; -use crate::api::ArrangerSceneCommand; /// Root view for standalone `tek_arranger` pub struct ArrangerTui { pub jack: Arc>, diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index a7e38ad2..6de320af 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -1,9 +1,9 @@ use super::*; use crate::{ + PhrasePoolCommand as Pool, tui::phrase_rename::PhraseRenameCommand as Rename, tui::phrase_length::PhraseLengthCommand as Length, tui::file_browser::FileBrowserCommand as Browse, - api::PhrasePoolCommand as Pool, }; use Ordering::Relaxed; From 99fb3f97320228060ef831d4e6ef3d3062328742 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 16:41:11 +0100 Subject: [PATCH 026/905] start upgrading arranger --- crates/tek/src/cli/cli_arranger.rs | 14 +- crates/tek/src/lib.rs | 2 +- crates/tek/src/midi/midi_scene.rs | 4 +- crates/tek/src/midi/midi_track.rs | 4 +- crates/tek/src/space/bsp.rs | 5 + crates/tek/src/time.rs | 1 + crates/tek/src/tui/app_arranger.rs | 359 +++++++++++++---------------- 7 files changed, 177 insertions(+), 212 deletions(-) diff --git a/crates/tek/src/cli/cli_arranger.rs b/crates/tek/src/cli/cli_arranger.rs index 4c11ea1f..10f8223c 100644 --- a/crates/tek/src/cli/cli_arranger.rs +++ b/crates/tek/src/cli/cli_arranger.rs @@ -29,18 +29,16 @@ impl ArrangerCli { let track_color_1 = ItemColor::random(); let track_color_2 = ItemColor::random(); for i in 0..self.tracks { - let _track = app.track_add( - None, - Some(track_color_1.mix(track_color_2, i as f32 / self.tracks as f32)) - )?; + let _track = app.track_add(None, Some( + track_color_1.mix(track_color_2, i as f32 / self.tracks as f32).into() + ))?; } let scene_color_1 = ItemColor::random(); let scene_color_2 = ItemColor::random(); for i in 0..self.scenes { - let _scene = app.scene_add( - None, - Some(scene_color_1.mix(scene_color_2, i as f32 / self.scenes as f32)) - )?; + let _scene = app.scene_add(None, Some( + scene_color_1.mix(scene_color_2, i as f32 / self.scenes as f32).into() + ))?; } Ok(app) })?)?; diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs index 782eac2e..abe73129 100644 --- a/crates/tek/src/lib.rs +++ b/crates/tek/src/lib.rs @@ -1,4 +1,4 @@ -pub mod core; pub(crate) use self::core::*; +pub mod core; pub(crate) use self::core::*; pub mod time; pub(crate) use self::time::*; pub mod space; pub(crate) use self::space::*; pub mod tui; pub(crate) use self::tui::*; diff --git a/crates/tek/src/midi/midi_scene.rs b/crates/tek/src/midi/midi_scene.rs index 2d464290..fe0825dc 100644 --- a/crates/tek/src/midi/midi_scene.rs +++ b/crates/tek/src/midi/midi_scene.rs @@ -3,7 +3,7 @@ use crate::*; pub trait HasScenes { fn scenes (&self) -> &Vec; fn scenes_mut (&mut self) -> &mut Vec; - fn scene_add (&mut self, name: Option<&str>, color: Option) -> Usually<&mut S>; + fn scene_add (&mut self, name: Option<&str>, color: Option) -> Usually<&mut S>; fn scene_del (&mut self, index: usize) { self.scenes_mut().remove(index); } @@ -42,7 +42,7 @@ pub enum ArrangerSceneCommand { pub trait ArrangerSceneApi: Sized { fn name (&self) -> &Arc>; fn clips (&self) -> &Vec>>>; - fn color (&self) -> ItemColor; + fn color (&self) -> ItemPalette; fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> { let mut total = 0; diff --git a/crates/tek/src/midi/midi_track.rs b/crates/tek/src/midi/midi_track.rs index 43229a71..0a4fc7e6 100644 --- a/crates/tek/src/midi/midi_track.rs +++ b/crates/tek/src/midi/midi_track.rs @@ -15,7 +15,7 @@ impl HasTracks for Vec { } pub trait ArrangerTracksApi: HasTracks { - fn track_add (&mut self, name: Option<&str>, color: Option)-> Usually<&mut T>; + fn track_add (&mut self, name: Option<&str>, color: Option)-> Usually<&mut T>; fn track_del (&mut self, index: usize); fn track_default_name (&self) -> String { format!("Track {}", self.tracks().len() + 1) @@ -41,7 +41,7 @@ pub trait ArrangerTrackApi: HasPlayer + Send + Sync + Sized { /// Preferred width of track column fn width_mut (&mut self) -> &mut usize; /// Identifying color of track - fn color (&self) -> ItemColor; + fn color (&self) -> ItemPalette; fn longest_name (tracks: &[Self]) -> usize { tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) diff --git a/crates/tek/src/space/bsp.rs b/crates/tek/src/space/bsp.rs index 181f7fbd..47474f1c 100644 --- a/crates/tek/src/space/bsp.rs +++ b/crates/tek/src/space/bsp.rs @@ -97,3 +97,8 @@ impl, Y: Render> Render for Bsp { }) } } + +#[cfg(test)] +mod test { + use super::*; +} diff --git a/crates/tek/src/time.rs b/crates/tek/src/time.rs index 58c3c798..4c37e243 100644 --- a/crates/tek/src/time.rs +++ b/crates/tek/src/time.rs @@ -38,6 +38,7 @@ pub const NOTE_DURATIONS: [(usize, &str);26] = [ (3456, "9/1"), (6144, "16/1"), ]; + /// Returns the next shorter length pub fn prev_note_length (pulses: usize) -> usize { for i in 1..=16 { let length = NOTE_DURATIONS[16-i].0; if length < pulses { return length } } diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 7c8920ce..867e786e 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -10,7 +10,7 @@ pub struct ArrangerTui { pub splits: [u16;2], pub selected: ArrangerSelection, pub mode: ArrangerMode, - pub color: ItemColor, + pub color: ItemPalette, pub entered: bool, pub size: Measure, pub cursor: (usize, usize), @@ -33,7 +33,7 @@ from_jack!(|jack| ArrangerTui Self { tracks: vec![], color: TuiTheme::bg().into(), history: vec![], - mode: ArrangerMode::Vertical(2), + mode: ArrangerMode::V(2), name: Arc::new(RwLock::new(String::new())), size: Measure::new(), cursor: (0, 0), @@ -46,28 +46,38 @@ from_jack!(|jack| ArrangerTui Self { perf: PerfModel::default(), focus: FocusState::Entered(ArrangerFocus::Transport(TransportFocus::PlayPause)), }); -has_clock!(|self:ArrangerTui|&self.clock); -has_phrases!(|self:ArrangerTui|self.phrases.phrases); -has_editor!(|self:ArrangerTui|self.editor); +has_clock!(|self: ArrangerTui|&self.clock); +has_phrases!(|self: ArrangerTui|self.phrases.phrases); +has_editor!(|self: ArrangerTui|self.editor); handle!(|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input)); render!(|self: ArrangerTui|{ - let arranger_focused = self.arranger_focused(); - let transport_focused = match self.focus.inner() { - ArrangerFocus::Transport(_) => true, _ => false - }; - let transport = TransportView::from((self, None, transport_focused)); - let with_transport = move|x|col!([transport, x]); - let border = Lozenge(Style::default() - .bg(TuiTheme::border_bg()) - .fg(TuiTheme::border_fg(arranger_focused))); - let arranger = move||border.wrap(Tui::grow_y(1, lay!(|add|{ + let arranger = ||lay!(|add|{ + add(&Fill::wh(Lozenge(Style::default() + .bg(TuiTheme::border_bg()) + .fg(TuiTheme::border_fg(true)))))?; + add(&self.size)?; match self.mode { - ArrangerMode::Horizontal => add(&arranger_content_horizontal(self))?, - ArrangerMode::Vertical(factor) => add(&arranger_content_vertical(self, factor))? - }; - add(&self.size) - }))); + ArrangerMode::H => todo!("horizontal arranger"), + ArrangerMode::V(factor) => add(&lay!([ + Align::se(Fill::wh(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(true), + format!("{}x{}", self.size.w(), self.size.h())) + ))), + Tui::bg(self.color.darkest.rgb, lay!(![ + ArrangerVColumnSeparator::from(self), + ArrangerVRowSeparator::from((self, factor)), + col!(![ + ArrangerVHeader::from(self), + ArrangerVContent::from((self, factor)), + ]), + ArrangerVCursor::from((self, factor)), + ])), + ])), + } + }); let with_pool = |x|Split::right(false, self.splits[1], PhraseListView(&self.phrases), x); + let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); + let transport = TransportView::from((self, None, true)); + let with_transport = |x|col!([row!(![&play, &transport]), &x]); with_transport(col!([Fixed::h(self.splits[0], arranger()), with_pool(&self.editor),])) }); audio!(|self: ArrangerTui, client, scope|{ @@ -112,7 +122,7 @@ audio!(|self: ArrangerTui, client, scope|{ Undo, Redo, Clear, - Color(ItemColor), + Color(ItemPalette), Clock(ClockCommand), Scene(ArrangerSceneCommand), Track(ArrangerTrackCommand), @@ -122,92 +132,9 @@ audio!(|self: ArrangerTui, client, scope|{ Phrases(PhrasesCommand), Editor(PhraseCommand), } -command!(|self:ArrangerCommand,state:ArrangerTui|{ - use ArrangerCommand::*; - match self { - Focus(cmd) => cmd.execute(state)?.map(Focus), - Scene(cmd) => cmd.execute(state)?.map(Scene), - Track(cmd) => cmd.execute(state)?.map(Track), - Clip(cmd) => cmd.execute(state)?.map(Clip), - Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases), - Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor), - Clock(cmd) => cmd.execute(state)?.map(Clock), - Zoom(_) => { todo!(); }, - Select(selected) => { - *state.selected_mut() = selected; - None - }, - _ => { todo!() } - } -}); -command!(|self:ArrangerSceneCommand,_state:ArrangerTui|None); -command!(|self:ArrangerTrackCommand,_state:ArrangerTui|None); -command!(|self:ArrangerClipCommand, _state:ArrangerTui|None); -pub trait ArrangerControl: TransportControl { - fn selected (&self) -> ArrangerSelection; - fn selected_mut (&mut self) -> &mut ArrangerSelection; - fn activate (&mut self) -> Usually<()>; - fn selected_phrase (&self) -> Option>>; - fn toggle_loop (&mut self); - fn randomize_color (&mut self); -} -impl ArrangerControl for ArrangerTui { - fn selected (&self) -> ArrangerSelection { - self.selected - } - fn selected_mut (&mut self) -> &mut ArrangerSelection { - &mut self.selected - } - fn activate (&mut self) -> Usually<()> { - if let ArrangerSelection::Scene(s) = self.selected { - for (t, track) in self.tracks.iter_mut().enumerate() { - let phrase = self.scenes[s].clips[t].clone(); - if track.player.play_phrase.is_some() || phrase.is_some() { - track.player.enqueue_next(phrase.as_ref()); - } - } - if self.clock().is_stopped() { - self.clock().play_from(Some(0))?; - } - } else if let ArrangerSelection::Clip(t, s) = self.selected { - let phrase = self.scenes()[s].clips[t].clone(); - self.tracks_mut()[t].player.enqueue_next(phrase.as_ref()); - }; - Ok(()) - } - fn selected_phrase (&self) -> Option>> { - self.selected_scene()?.clips.get(self.selected.track()?)?.clone() - } - fn toggle_loop (&mut self) { - if let Some(phrase) = self.selected_phrase() { - phrase.write().unwrap().toggle_loop() - } - } - fn randomize_color (&mut self) { - match self.selected { - ArrangerSelection::Mix => { - self.color = ItemColor::random_dark() - }, - ArrangerSelection::Track(t) => { - self.tracks_mut()[t].color = ItemColor::random() - }, - ArrangerSelection::Scene(s) => { - self.scenes_mut()[s].color = ItemColor::random() - }, - ArrangerSelection::Clip(t, s) => { - if let Some(phrase) = &self.scenes_mut()[s].clips[t] { - phrase.write().unwrap().color = ItemPalette::random(); - } - } - } - } -} -impl InputToCommand for ArrangerCommand { - fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option { - to_arranger_command(state, input) - .or_else(||to_focus_command(input).map(ArrangerCommand::Focus)) - } -} +input_to_command!(ArrangerCommand: |state:ArrangerTui,input| + to_arranger_command(state, input) + .or_else(||to_focus_command(input).map(ArrangerCommand::Focus))?); fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option { use ArrangerCommand as Cmd; use KeyCode::Char; @@ -264,7 +191,7 @@ fn to_arranger_mix_command (input: &TuiInput) -> Option { key_pat!(Char('<')) => Cmd::Zoom(0), key_pat!(Char('>')) => Cmd::Zoom(0), key_pat!(Delete) => Cmd::Clear, - key_pat!(Char('c')) => Cmd::Color(ItemColor::random()), + key_pat!(Char('c')) => Cmd::Color(ItemPalette::random()), _ => return None }) } @@ -282,7 +209,7 @@ fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option Cmd::Track(Track::Swap(t, t - 1)), key_pat!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)), key_pat!(Delete) => Cmd::Track(Track::Delete(t)), - //key_pat!(Char('c')) => Cmd::Track(Track::Color(t, ItemColor::random())), + //key_pat!(Char('c')) => Cmd::Track(Track::Color(t, ItemPalette::random())), _ => return None }) } @@ -301,7 +228,7 @@ fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option')) => Cmd::Scene(Scene::Swap(s, s + 1)), key_pat!(Enter) => Cmd::Scene(Scene::Play(s)), key_pat!(Delete) => Cmd::Scene(Scene::Delete(s)), - //key_pat!(Char('c')) => Cmd::Track(Scene::Color(s, ItemColor::random())), + //key_pat!(Char('c')) => Cmd::Track(Scene::Color(s, ItemPalette::random())), _ => return None }) } @@ -320,12 +247,92 @@ fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option Cmd::Clip(Clip::Set(t, s, None)), key_pat!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)), key_pat!(Delete) => Cmd::Clip(Clip::Set(t, s, None)), - //key_pat!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemColor::random())), + //key_pat!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemPalette::random())), //key_pat!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))), //key_pat!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))), _ => return None }) } +command!(|self:ArrangerCommand,state:ArrangerTui|{ + use ArrangerCommand::*; + match self { + Focus(cmd) => cmd.execute(state)?.map(Focus), + Scene(cmd) => cmd.execute(state)?.map(Scene), + Track(cmd) => cmd.execute(state)?.map(Track), + Clip(cmd) => cmd.execute(state)?.map(Clip), + Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases), + Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor), + Clock(cmd) => cmd.execute(state)?.map(Clock), + Zoom(_) => { todo!(); }, + Select(selected) => { + *state.selected_mut() = selected; + None + }, + _ => { todo!() } + } +}); +command!(|self:ArrangerSceneCommand,_state:ArrangerTui|None); +command!(|self:ArrangerTrackCommand,_state:ArrangerTui|None); +command!(|self:ArrangerClipCommand, _state:ArrangerTui|None); +pub trait ArrangerControl: TransportControl { + fn selected (&self) -> ArrangerSelection; + fn selected_mut (&mut self) -> &mut ArrangerSelection; + fn selected_phrase (&self) -> Option>>; + fn activate (&mut self) -> Usually<()>; + fn toggle_loop (&mut self); + fn randomize_color (&mut self); +} +impl ArrangerControl for ArrangerTui { + fn selected (&self) -> ArrangerSelection { + self.selected + } + fn selected_mut (&mut self) -> &mut ArrangerSelection { + &mut self.selected + } + fn activate (&mut self) -> Usually<()> { + if let ArrangerSelection::Scene(s) = self.selected { + for (t, track) in self.tracks.iter_mut().enumerate() { + let phrase = self.scenes[s].clips[t].clone(); + if track.player.play_phrase.is_some() || phrase.is_some() { + track.player.enqueue_next(phrase.as_ref()); + } + } + if self.clock().is_stopped() { + self.clock().play_from(Some(0))?; + } + } else if let ArrangerSelection::Clip(t, s) = self.selected { + let phrase = self.scenes()[s].clips[t].clone(); + self.tracks_mut()[t].player.enqueue_next(phrase.as_ref()); + }; + Ok(()) + } + fn selected_phrase (&self) -> Option>> { + self.selected_scene()?.clips.get(self.selected.track()?)?.clone() + } + fn toggle_loop (&mut self) { + if let Some(phrase) = self.selected_phrase() { + phrase.write().unwrap().toggle_loop() + } + } + fn randomize_color (&mut self) { + match self.selected { + ArrangerSelection::Mix => { + self.color = ItemPalette::random() + }, + ArrangerSelection::Track(t) => { + self.tracks_mut()[t].color = ItemPalette::random() + }, + ArrangerSelection::Scene(s) => { + self.scenes_mut()[s].color = ItemPalette::random() + }, + ArrangerSelection::Clip(t, s) => { + if let Some(phrase) = &self.scenes_mut()[s].clips[t] { + phrase.write().unwrap().color = ItemPalette::random(); + } + } + } + } +} impl TransportControl for ArrangerTui { fn transport_focused (&self) -> Option { match self.focus.inner() { @@ -368,32 +375,15 @@ impl From<&ArrangerTui> for Option { } impl_focus!(ArrangerTui ArrangerFocus [ - //&[ - //Menu, - //Menu, - //Menu, - //Menu, - //Menu, - //], &[ Transport(TransportFocus::PlayPause), Transport(TransportFocus::Bpm), Transport(TransportFocus::Sync), Transport(TransportFocus::Quant), Transport(TransportFocus::Clock), - ], &[ - Arranger, - Arranger, - Arranger, - Arranger, - Arranger, - ], &[ - Phrases, - Phrases, - PhraseEditor, - PhraseEditor, - PhraseEditor, ], + &[Arranger;5], + &[Phrases, Phrases, PhraseEditor, PhraseEditor, PhraseEditor], ]); /// Status bar for arranger app @@ -412,10 +402,10 @@ pub enum ArrangerStatus { /// Display mode of arranger #[derive(Clone, PartialEq)] pub enum ArrangerMode { - /// Tracks are rows - Horizontal, /// Tracks are columns - Vertical(usize), + V(usize), + /// Tracks are rows + H, } /// Arranger display mode can be cycled @@ -423,24 +413,15 @@ impl ArrangerMode { /// 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), + Self::H => Self::V(1), + Self::V(1) => Self::V(2), + Self::V(2) => Self::V(2), + Self::V(0) => Self::H, + Self::V(_) => Self::V(0), } } } -pub trait ArrangerViewState { - fn arranger_focused (&self) -> bool; -} -impl ArrangerViewState for ArrangerTui { - fn arranger_focused (&self) -> bool { - self.focused() == ArrangerFocus::Arranger - } -} - fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { let mut widths = vec![]; let mut total = 0; @@ -457,32 +438,12 @@ fn any_size (_: E::Size) -> Perhaps{ Ok(Some([0.into(),0.into()].into())) } -pub fn arranger_content_vertical ( - view: &ArrangerTui, - factor: usize -) -> impl Render + use<'_> { - lay!([ - Align::se(Fill::wh(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(view.arranger_focused()), - format!("{}x{}", view.size.w(), view.size.h())) - ))), - Tui::bg(view.color.rgb, lay!(![ - ArrangerVerticalColumnSeparator::from(view), - ArrangerVerticalRowSeparator::from((view, factor)), - col!(![ - ArrangerVerticalHeader::from(view), - ArrangerVerticalContent::from((view, factor)), - ]), - ArrangerVerticalCursor::from((view, factor)), - ])), - ]) -} - -struct ArrangerVerticalColumnSeparator { +struct ArrangerVColumnSeparator { cols: Vec<(usize, usize)>, scenes_w: u16, sep_fg: Color, } -impl From<&ArrangerTui> for ArrangerVerticalColumnSeparator { +impl From<&ArrangerTui> for ArrangerVColumnSeparator { fn from (state: &ArrangerTui) -> Self { Self { cols: track_widths(state.tracks()), @@ -491,7 +452,7 @@ impl From<&ArrangerTui> for ArrangerVerticalColumnSeparator { } } } -render!(|self: ArrangerVerticalColumnSeparator|render(move|to: &mut TuiOutput|{ +render!(|self: ArrangerVColumnSeparator|render(move|to: &mut TuiOutput|{ let style = Some(Style::default().fg(self.sep_fg)); Ok(for x in self.cols.iter().map(|col|col.1) { let x = self.scenes_w + to.area().x() + x as u16; @@ -501,11 +462,11 @@ render!(|self: ArrangerVerticalColumnSeparator|render(move|to: &mut TuiOutp }) })); -struct ArrangerVerticalRowSeparator { +struct ArrangerVRowSeparator { rows: Vec<(usize, usize)>, sep_fg: Color, } -impl From<(&ArrangerTui, usize)> for ArrangerVerticalRowSeparator { +impl From<(&ArrangerTui, usize)> for ArrangerVRowSeparator { fn from ((state, factor): (&ArrangerTui, usize)) -> Self { Self { rows: ArrangerScene::ppqs(state.scenes(), factor), @@ -514,7 +475,7 @@ impl From<(&ArrangerTui, usize)> for ArrangerVerticalRowSeparator { } } -render!(|self: ArrangerVerticalRowSeparator|render(move|to: &mut TuiOutput|{ +render!(|self: ArrangerVRowSeparator|render(move|to: &mut TuiOutput|{ Ok(for y in self.rows.iter().map(|row|row.1) { let y = to.area().y() + (y / PPQ) as u16 + 1; if y >= to.buffer.area.height { break } @@ -528,7 +489,7 @@ render!(|self: ArrangerVerticalRowSeparator|render(move|to: &mut TuiOutput| }) })); -struct ArrangerVerticalCursor { +struct ArrangerVCursor { cols: Vec<(usize, usize)>, rows: Vec<(usize, usize)>, focused: bool, @@ -536,19 +497,19 @@ struct ArrangerVerticalCursor { scenes_w: u16, header_h: u16, } -impl From<(&ArrangerTui, usize)> for ArrangerVerticalCursor { +impl From<(&ArrangerTui, usize)> for ArrangerVCursor { fn from ((state, factor): (&ArrangerTui, usize)) -> Self { Self { cols: track_widths(state.tracks()), rows: ArrangerScene::ppqs(state.scenes(), factor), - focused: state.arranger_focused(), + focused: true, selected: state.selected, scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, header_h: 3, } } } -render!(|self: ArrangerVerticalCursor|render(move|to: &mut TuiOutput|{ +render!(|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{ let area = to.area(); let focused = self.focused; let selected = self.selected; @@ -603,7 +564,7 @@ render!(|self: ArrangerVerticalCursor|render(move|to: &mut TuiOutput|{ }) })); -struct ArrangerVerticalHeader<'a> { +struct ArrangerVHeader<'a> { tracks: &'a Vec, cols: Vec<(usize, usize)>, focused: bool, @@ -613,12 +574,12 @@ struct ArrangerVerticalHeader<'a> { timebase: &'a Arc, current: &'a Arc, } -impl<'a> From<&'a ArrangerTui> for ArrangerVerticalHeader<'a> { +impl<'a> From<&'a ArrangerTui> for ArrangerVHeader<'a> { fn from (state: &'a ArrangerTui) -> Self { Self { tracks: &state.tracks, cols: track_widths(state.tracks()), - focused: state.arranger_focused(), + focused: true, selected: state.selected, scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, header_h: 3, @@ -627,13 +588,13 @@ impl<'a> From<&'a ArrangerTui> for ArrangerVerticalHeader<'a> { } } } -render!(|self: ArrangerVerticalHeader<'a>|row!( +render!(|self: ArrangerVHeader<'a>|row!( (track, w) in self.tracks.iter().zip(self.cols.iter().map(|col|col.0)) => { // name and width of track let name = track.name().read().unwrap(); let max_w = w.saturating_sub(1).min(name.len()).max(2); let name = format!("▎{}", &name[0..max_w]); - let name = Tui::bold(true, name); + let name = Tui::bold(true, Tui::fg(track.color.lightest.rgb, name)); // beats elapsed let elapsed = if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() { let length = phrase.read().unwrap().length; @@ -668,13 +629,13 @@ render!(|self: ArrangerVerticalHeader<'a>|row!( .transpose()? .unwrap_or("(none)".into())); Tui::push_x(self.scenes_w, - Tui::bg(track.color().rgb, + Tui::bg(track.color().base.rgb, Tui::min_xy(w as u16, self.header_h, col!([name, timer])))) } )); -struct ArrangerVerticalContent<'a> { +struct ArrangerVContent<'a> { size: &'a Measure, scenes: &'a Vec, tracks: &'a Vec, @@ -682,7 +643,7 @@ struct ArrangerVerticalContent<'a> { cols: Vec<(usize, usize)>, header_h: u16, } -impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVerticalContent<'a> { +impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVContent<'a> { fn from ((state, factor): (&'a ArrangerTui, usize)) -> Self { Self { size: &state.size, @@ -694,7 +655,7 @@ impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVerticalContent<'a> { } } } -render!(|self: ArrangerVerticalContent<'a>|Fixed::h( +render!(|self: ArrangerVContent<'a>|Fixed::h( (self.size.h() as u16).saturating_sub(self.header_h), col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => { let height = 1.max((pulses / PPQ) as u16); @@ -736,14 +697,14 @@ impl HasScenes for ArrangerTui { fn scenes_mut (&mut self) -> &mut Vec { &mut self.scenes } - fn scene_add (&mut self, name: Option<&str>, color: Option) + fn scene_add (&mut self, name: Option<&str>, color: Option) -> Usually<&mut ArrangerScene> { let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string()); let scene = ArrangerScene { name: Arc::new(name.into()), clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(||ItemColor::random()), + color: color.unwrap_or_else(||ItemPalette::random()), }; self.scenes_mut().push(scene); let index = self.scenes().len() - 1; @@ -763,7 +724,7 @@ impl HasScenes for ArrangerTui { /// Clips in scene, one per track pub(crate) clips: Vec>>>, /// Identifying color of scene - pub(crate) color: ItemColor, + pub(crate) color: ItemPalette, } impl ArrangerSceneApi for ArrangerScene { fn name (&self) -> &Arc> { @@ -772,7 +733,7 @@ impl ArrangerSceneApi for ArrangerScene { fn clips (&self) -> &Vec>>> { &self.clips } - fn color (&self) -> ItemColor { + fn color (&self) -> ItemPalette { self.color } } @@ -785,14 +746,14 @@ impl HasTracks for ArrangerTui { } } impl ArrangerTracksApi for ArrangerTui { - fn track_add (&mut self, name: Option<&str>, color: Option) + fn track_add (&mut self, name: Option<&str>, color: Option) -> Usually<&mut ArrangerTrack> { let name = name.map_or_else(||self.track_default_name(), |x|x.to_string()); let track = ArrangerTrack { width: name.len() + 2, name: Arc::new(name.into()), - color: color.unwrap_or_else(||ItemColor::random()), + color: color.unwrap_or_else(||ItemPalette::random()), player: PhrasePlayerModel::from(&self.clock), }; self.tracks_mut().push(track); @@ -813,7 +774,7 @@ impl ArrangerTracksApi for ArrangerTui { /// Preferred width of track column pub(crate) width: usize, /// Identifying color of track - pub(crate) color: ItemColor, + pub(crate) color: ItemPalette, /// MIDI player state pub(crate) player: PhrasePlayerModel, } @@ -833,7 +794,7 @@ impl ArrangerTrackApi for ArrangerTrack { &mut self.width } /// Identifying color of track - fn color (&self) -> ItemColor { + fn color (&self) -> ItemPalette { self.color } } @@ -930,7 +891,7 @@ pub fn arranger_content_horizontal ( ) -> impl Render + use<'_> { todo!() } - //let focused = view.arranger_focused(); + //let focused = true; //let _tracks = view.tracks(); //lay!( //focused.then_some(Background(TuiTheme::border_bg())), @@ -1137,13 +1098,13 @@ pub fn arranger_content_horizontal ( //AddTrack => { state.state.track_add(None, None)?; }, //ToggleLoop => { state.state.toggle_loop() }, //pub fn zoom_in (&mut self) { - //if let ArrangerEditorMode::Vertical(factor) = self.mode { - //self.mode = ArrangerEditorMode::Vertical(factor + 1) + //if let ArrangerEditorMode::V(factor) = self.mode { + //self.mode = ArrangerEditorMode::V(factor + 1) //} //} //pub fn zoom_out (&mut self) { - //if let ArrangerEditorMode::Vertical(factor) = self.mode { - //self.mode = ArrangerEditorMode::Vertical(factor.saturating_sub(1)) + //if let ArrangerEditorMode::V(factor) = self.mode { + //self.mode = ArrangerEditorMode::V(factor.saturating_sub(1)) //} //} //pub fn move_back (&mut self) { From 4ee9822213b3960d072a8e64a7fce2f3118e5e62 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 16:55:46 +0100 Subject: [PATCH 027/905] fix setting of arranger color --- crates/tek/src/tui/app_arranger.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 867e786e..c326cd01 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -33,11 +33,11 @@ from_jack!(|jack| ArrangerTui Self { tracks: vec![], color: TuiTheme::bg().into(), history: vec![], - mode: ArrangerMode::V(2), + mode: ArrangerMode::V(1), name: Arc::new(RwLock::new(String::new())), size: Measure::new(), cursor: (0, 0), - splits: [20, 20], + splits: [16, 20], entered: false, menu_bar: None, status_bar: None, @@ -62,7 +62,7 @@ render!(|self: ArrangerTui|{ Align::se(Fill::wh(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(true), format!("{}x{}", self.size.w(), self.size.h())) ))), - Tui::bg(self.color.darkest.rgb, lay!(![ + Tui::bg(self.color.dark.rgb, lay!(![ ArrangerVColumnSeparator::from(self), ArrangerVRowSeparator::from((self, factor)), col!(![ @@ -268,6 +268,11 @@ command!(|self:ArrangerCommand,state:ArrangerTui|{ *state.selected_mut() = selected; None }, + Color(palette) => { + let old = state.color; + state.color = palette; + Some(Color(old)) + }, _ => { todo!() } } }); From 8472805142be13268ea46c3b0ee373598e19b069 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 18:21:45 +0100 Subject: [PATCH 028/905] refactor arranger --- crates/tek/src/cli/cli_arranger.rs | 1 + crates/tek/src/tui.rs | 9 + crates/tek/src/tui/app_arranger.rs | 723 ++------------------------ crates/tek/src/tui/arranger_clip.rs | 23 + crates/tek/src/tui/arranger_mode_h.rs | 0 crates/tek/src/tui/arranger_mode_v.rs | 274 ++++++++++ crates/tek/src/tui/arranger_scene.rs | 66 +++ crates/tek/src/tui/arranger_select.rs | 70 +++ crates/tek/src/tui/arranger_track.rs | 80 +++ crates/tek/src/tui/status_bar.rs | 229 ++++---- 10 files changed, 701 insertions(+), 774 deletions(-) create mode 100644 crates/tek/src/tui/arranger_clip.rs create mode 100644 crates/tek/src/tui/arranger_mode_h.rs create mode 100644 crates/tek/src/tui/arranger_mode_v.rs create mode 100644 crates/tek/src/tui/arranger_scene.rs create mode 100644 crates/tek/src/tui/arranger_select.rs create mode 100644 crates/tek/src/tui/arranger_track.rs diff --git a/crates/tek/src/cli/cli_arranger.rs b/crates/tek/src/cli/cli_arranger.rs index 10f8223c..dde92c12 100644 --- a/crates/tek/src/cli/cli_arranger.rs +++ b/crates/tek/src/cli/cli_arranger.rs @@ -23,6 +23,7 @@ impl ArrangerCli { fn run (&self) -> Usually<()> { Tui::run(JackClient::new("tek_arranger")?.activate_with(|jack|{ let mut app = ArrangerTui::try_from(jack)?; + app.color = ItemPalette::random(); if let Some(name) = self.name.as_ref() { *app.name.write().unwrap() = name.clone(); } diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index 4534c36a..fcf1ba66 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -25,6 +25,15 @@ mod app_sampler; pub(crate) use app_sampler::*; mod app_groovebox; pub(crate) use app_groovebox::*; mod app_arranger; pub(crate) use app_arranger::*; +/////////////////////////////////////////////////////// + +mod arranger_clip; pub(crate) use arranger_clip::*; +mod arranger_scene; pub(crate) use arranger_scene::*; +mod arranger_select; pub(crate) use arranger_select::*; +mod arranger_track; pub(crate) use arranger_track::*; +mod arranger_mode_h; pub(crate) use arranger_mode_h::*; +mod arranger_mode_v; pub(crate) use arranger_mode_v::*; + //////////////////////////////////////////////////////// mod status_bar; pub(crate) use status_bar::*; diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index c326cd01..20c27cd4 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -11,7 +11,6 @@ pub struct ArrangerTui { pub selected: ArrangerSelection, pub mode: ArrangerMode, pub color: ItemPalette, - pub entered: bool, pub size: Measure, pub cursor: (usize, usize), pub menu_bar: Option>, @@ -20,7 +19,6 @@ pub struct ArrangerTui { pub note_buf: Vec, pub midi_buf: Vec>>, pub editor: PhraseEditorModel, - pub focus: FocusState, pub perf: PerfModel, } from_jack!(|jack| ArrangerTui Self { @@ -38,13 +36,11 @@ from_jack!(|jack| ArrangerTui Self { size: Measure::new(), cursor: (0, 0), splits: [16, 20], - entered: false, menu_bar: None, status_bar: None, midi_buf: vec![vec![];65536], note_buf: vec![], perf: PerfModel::default(), - focus: FocusState::Entered(ArrangerFocus::Transport(TransportFocus::PlayPause)), }); has_clock!(|self: ArrangerTui|&self.clock); has_phrases!(|self: ArrangerTui|self.phrases.phrases); @@ -52,9 +48,8 @@ has_editor!(|self: ArrangerTui|self.editor); handle!(|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input)); render!(|self: ArrangerTui|{ let arranger = ||lay!(|add|{ - add(&Fill::wh(Lozenge(Style::default() - .bg(TuiTheme::border_bg()) - .fg(TuiTheme::border_fg(true)))))?; + let color = self.color; + add(&Fill::wh(Lozenge(Style::default().bg(color.light.rgb).fg(color.darker.rgb))))?; add(&self.size)?; match self.mode { ArrangerMode::H => todo!("horizontal arranger"), @@ -62,7 +57,7 @@ render!(|self: ArrangerTui|{ Align::se(Fill::wh(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(true), format!("{}x{}", self.size.w(), self.size.h())) ))), - Tui::bg(self.color.dark.rgb, lay!(![ + Tui::bg(color.darkest.rgb, lay!(![ ArrangerVColumnSeparator::from(self), ArrangerVRowSeparator::from((self, factor)), col!(![ @@ -118,7 +113,6 @@ audio!(|self: ArrangerTui, client, scope|{ return Control::Continue }); #[derive(Clone, Debug)] pub enum ArrangerCommand { - Focus(FocusCommand), Undo, Redo, Clear, @@ -132,50 +126,27 @@ audio!(|self: ArrangerTui, client, scope|{ Phrases(PhrasesCommand), Editor(PhraseCommand), } -input_to_command!(ArrangerCommand: |state:ArrangerTui,input| - to_arranger_command(state, input) - .or_else(||to_focus_command(input).map(ArrangerCommand::Focus))?); +input_to_command!(ArrangerCommand: |state:ArrangerTui,input|to_arranger_command(state, input)?); fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option { + use ArrangerSelection::*; use ArrangerCommand as Cmd; use KeyCode::Char; - if !state.entered() { - return None - } + // WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor Some(match input.event() { key_pat!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))), - // WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor - _ => match state.focused() { - ArrangerFocus::Transport(_) => { - match to_transport_command(state, input)? { - TransportCommand::Clock(command) => Cmd::Clock(command), - _ => return None, - } - }, - ArrangerFocus::PhraseEditor => { - Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?) - }, - ArrangerFocus::Phrases => { - Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?) - }, - ArrangerFocus::Arranger => { - use ArrangerSelection::*; - match input.event() { - key_pat!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)), - key_pat!(Char('+')) => Cmd::Zoom(0), // TODO - key_pat!(Char('=')) => Cmd::Zoom(0), // TODO - key_pat!(Char('_')) => Cmd::Zoom(0), // TODO - key_pat!(Char('-')) => Cmd::Zoom(0), // TODO - key_pat!(Char('`')) => { todo!("toggle state mode") }, - key_pat!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add), - key_pat!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add), - _ => match state.selected() { - Mix => to_arranger_mix_command(input)?, - Track(t) => to_arranger_track_command(input, t)?, - Scene(s) => to_arranger_scene_command(input, s)?, - Clip(t, s) => to_arranger_clip_command(input, t, s)?, - } - } - } + key_pat!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)), + key_pat!(Char('+')) => Cmd::Zoom(0), // TODO + key_pat!(Char('=')) => Cmd::Zoom(0), // TODO + key_pat!(Char('_')) => Cmd::Zoom(0), // TODO + key_pat!(Char('-')) => Cmd::Zoom(0), // TODO + key_pat!(Char('`')) => { todo!("toggle state mode") }, + key_pat!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add), + key_pat!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add), + _ => match state.selected() { + Mix => to_arranger_mix_command(input)?, + Track(t) => to_arranger_track_command(input, t)?, + Scene(s) => to_arranger_scene_command(input, s)?, + Clip(t, s) => to_arranger_clip_command(input, t, s)?, } }) } @@ -184,8 +155,8 @@ fn to_arranger_mix_command (input: &TuiInput) -> Option { use ArrangerCommand as Cmd; use ArrangerSelection as Select; Some(match input.event() { - key_pat!(Down) => Cmd::Select(Select::Scene(0)), - key_pat!(Right) => Cmd::Select(Select::Track(0)), + key_pat!(Char('s')) => Cmd::Select(Select::Scene(0)), + key_pat!(Char('d')) => Cmd::Select(Select::Track(0)), key_pat!(Char(',')) => Cmd::Zoom(0), key_pat!(Char('.')) => Cmd::Zoom(0), key_pat!(Char('<')) => Cmd::Zoom(0), @@ -195,68 +166,9 @@ fn to_arranger_mix_command (input: &TuiInput) -> Option { _ => return None }) } -fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option { - use KeyCode::{Char, Down, Left, Right, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerTrackCommand as Track; - Some(match input.event() { - key_pat!(Down) => Cmd::Select(Select::Clip(t, 0)), - key_pat!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }), - key_pat!(Right) => Cmd::Select(Select::Track(t + 1)), - key_pat!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)), - key_pat!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)), - key_pat!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)), - key_pat!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)), - key_pat!(Delete) => Cmd::Track(Track::Delete(t)), - //key_pat!(Char('c')) => Cmd::Track(Track::Color(t, ItemPalette::random())), - _ => return None - }) -} -fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { - use KeyCode::{Char, Up, Down, Right, Enter, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerSceneCommand as Scene; - Some(match input.event() { - key_pat!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }), - key_pat!(Down) => Cmd::Select(Select::Scene(s + 1)), - key_pat!(Right) => Cmd::Select(Select::Clip(0, s)), - key_pat!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)), - key_pat!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)), - key_pat!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)), - key_pat!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)), - key_pat!(Enter) => Cmd::Scene(Scene::Play(s)), - key_pat!(Delete) => Cmd::Scene(Scene::Delete(s)), - //key_pat!(Char('c')) => Cmd::Track(Scene::Color(s, ItemPalette::random())), - _ => return None - }) -} -fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option { - use KeyCode::{Char, Up, Down, Left, Right, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerClipCommand as Clip; - Some(match input.event() { - key_pat!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }), - key_pat!(Down) => Cmd::Select(Select::Clip(t, s + 1)), - key_pat!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }), - key_pat!(Right) => Cmd::Select(Select::Clip(t + 1, s)), - key_pat!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)), - key_pat!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)), - key_pat!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)), - key_pat!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)), - key_pat!(Delete) => Cmd::Clip(Clip::Set(t, s, None)), - //key_pat!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemPalette::random())), - //key_pat!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))), - //key_pat!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))), - _ => return None - }) -} command!(|self:ArrangerCommand,state:ArrangerTui|{ use ArrangerCommand::*; match self { - Focus(cmd) => cmd.execute(state)?.map(Focus), Scene(cmd) => cmd.execute(state)?.map(Scene), Track(cmd) => cmd.execute(state)?.map(Track), Clip(cmd) => cmd.execute(state)?.map(Clip), @@ -279,15 +191,43 @@ command!(|self:ArrangerCommand,state:ArrangerTui|{ command!(|self:ArrangerSceneCommand,_state:ArrangerTui|None); command!(|self:ArrangerTrackCommand,_state:ArrangerTui|None); command!(|self:ArrangerClipCommand, _state:ArrangerTui|None); -pub trait ArrangerControl: TransportControl { - fn selected (&self) -> ArrangerSelection; - fn selected_mut (&mut self) -> &mut ArrangerSelection; - fn selected_phrase (&self) -> Option>>; - fn activate (&mut self) -> Usually<()>; - fn toggle_loop (&mut self); - fn randomize_color (&mut self); +#[derive(Clone, Debug)] +pub enum ArrangerClipCommand { + Play, + Get(usize, usize), + Set(usize, usize, Option>>), + Edit(Option>>), + SetLoop(bool), + RandomColor, } -impl ArrangerControl for ArrangerTui { + +/// Display mode of arranger +#[derive(Clone, PartialEq)] +pub enum ArrangerMode { + /// Tracks are columns + V(usize), + /// Tracks are rows + H, +} + +/// Arranger display mode can be cycled +impl ArrangerMode { + /// Cycle arranger display mode + pub fn to_next (&mut self) { + *self = match self { + Self::H => Self::V(1), + Self::V(1) => Self::V(2), + Self::V(2) => Self::V(2), + Self::V(0) => Self::H, + Self::V(_) => Self::V(0), + } + } +} + +fn any_size (_: E::Size) -> Perhaps{ + Ok(Some([0.into(),0.into()].into())) +} +impl ArrangerTui { fn selected (&self) -> ArrangerSelection { self.selected } @@ -338,549 +278,6 @@ impl ArrangerControl for ArrangerTui { } } } -impl TransportControl for ArrangerTui { - fn transport_focused (&self) -> Option { - match self.focus.inner() { - ArrangerFocus::Transport(focus) => Some(focus), - _ => None - } - } -} - -/// Sections in the arranger app that may be focused -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum ArrangerFocus { - /// The transport (toolbar) is focused - Transport(TransportFocus), - /// The arrangement (grid) is focused - Arranger, - /// The phrase list (pool) is focused - Phrases, - /// The phrase editor (sequencer) is focused - PhraseEditor, -} - -impl Into> for ArrangerFocus { - fn into (self) -> Option { - if let Self::Transport(transport) = self { - Some(transport) - } else { - None - } - } -} - -impl From<&ArrangerTui> for Option { - fn from (state: &ArrangerTui) -> Self { - match state.focus.inner() { - ArrangerFocus::Transport(focus) => Some(focus), - _ => None - } - } -} - -impl_focus!(ArrangerTui ArrangerFocus [ - &[ - Transport(TransportFocus::PlayPause), - Transport(TransportFocus::Bpm), - Transport(TransportFocus::Sync), - Transport(TransportFocus::Quant), - Transport(TransportFocus::Clock), - ], - &[Arranger;5], - &[Phrases, Phrases, PhraseEditor, PhraseEditor, PhraseEditor], -]); - -/// Status bar for arranger app -#[derive(Copy, Clone, Debug)] -pub enum ArrangerStatus { - Transport, - ArrangerMix, - ArrangerTrack, - ArrangerScene, - ArrangerClip, - PhrasePool, - PhraseView, - PhraseEdit, -} - -/// Display mode of arranger -#[derive(Clone, PartialEq)] -pub enum ArrangerMode { - /// Tracks are columns - V(usize), - /// Tracks are rows - H, -} - -/// Arranger display mode can be cycled -impl ArrangerMode { - /// Cycle arranger display mode - pub fn to_next (&mut self) { - *self = match self { - Self::H => Self::V(1), - Self::V(1) => Self::V(2), - Self::V(2) => Self::V(2), - Self::V(0) => Self::H, - Self::V(_) => Self::V(0), - } - } -} - -fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { - let mut widths = vec![]; - let mut total = 0; - for track in tracks.iter() { - let width = track.width; - widths.push((width, total)); - total += width; - } - widths.push((0, total)); - widths -} - -fn any_size (_: E::Size) -> Perhaps{ - Ok(Some([0.into(),0.into()].into())) -} - -struct ArrangerVColumnSeparator { - cols: Vec<(usize, usize)>, - scenes_w: u16, - sep_fg: Color, -} -impl From<&ArrangerTui> for ArrangerVColumnSeparator { - fn from (state: &ArrangerTui) -> Self { - Self { - cols: track_widths(state.tracks()), - scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, - sep_fg: TuiTheme::separator_fg(false), - } - } -} -render!(|self: ArrangerVColumnSeparator|render(move|to: &mut TuiOutput|{ - let style = Some(Style::default().fg(self.sep_fg)); - Ok(for x in self.cols.iter().map(|col|col.1) { - let x = self.scenes_w + to.area().x() + x as u16; - for y in to.area().y()..to.area().y2() { - to.blit(&"▎", x, y, style); - } - }) -})); - -struct ArrangerVRowSeparator { - rows: Vec<(usize, usize)>, - sep_fg: Color, -} -impl From<(&ArrangerTui, usize)> for ArrangerVRowSeparator { - fn from ((state, factor): (&ArrangerTui, usize)) -> Self { - Self { - rows: ArrangerScene::ppqs(state.scenes(), factor), - sep_fg: TuiTheme::separator_fg(false), - } - } -} - -render!(|self: ArrangerVRowSeparator|render(move|to: &mut TuiOutput|{ - Ok(for y in self.rows.iter().map(|row|row.1) { - let y = to.area().y() + (y / PPQ) as u16 + 1; - if y >= to.buffer.area.height { break } - for x in to.area().x()..to.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 = self.sep_fg; - } - } - }) -})); - -struct ArrangerVCursor { - cols: Vec<(usize, usize)>, - rows: Vec<(usize, usize)>, - focused: bool, - selected: ArrangerSelection, - scenes_w: u16, - header_h: u16, -} -impl From<(&ArrangerTui, usize)> for ArrangerVCursor { - fn from ((state, factor): (&ArrangerTui, usize)) -> Self { - Self { - cols: track_widths(state.tracks()), - rows: ArrangerScene::ppqs(state.scenes(), factor), - focused: true, - selected: state.selected, - scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, - header_h: 3, - } - } -} -render!(|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{ - let area = to.area(); - let focused = self.focused; - let selected = self.selected; - let get_track_area = |t: usize| [ - self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(), - self.cols[t].0 as u16, area.h(), - ]; - let get_scene_area = |s: usize| [ - area.x(), self.header_h + area.y() + (self.rows[s].1 / PPQ) as u16, - area.w(), (self.rows[s].0 / PPQ) as u16 - ]; - let get_clip_area = |t: usize, s: usize| [ - self.scenes_w + area.x() + self.cols[t].1 as u16, - self.header_h + area.y() + (self.rows[s].1/PPQ) as u16, - self.cols[t].0 as u16, - (self.rows[s].0 / PPQ) 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 { - ArrangerSelection::Mix => area, - ArrangerSelection::Track(t) => { - track_area = Some(get_track_area(t)); - area - }, - ArrangerSelection::Scene(s) => { - scene_area = Some(get_scene_area(s)); - area - }, - ArrangerSelection::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 - }, - }; - let bg = TuiTheme::border_bg(); - if let Some([x, y, width, height]) = track_area { - to.fill_fg([x, y, 1, height], bg); - to.fill_fg([x + width, y, 1, height], bg); - } - if let Some([_, y, _, height]) = scene_area { - to.fill_ul([area.x(), y - 1, area.w(), 1], bg); - to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg); - } - Ok(if focused { - to.render_in(if let Some(clip_area) = clip_area { clip_area } - else if let Some(track_area) = track_area { track_area.clip_h(self.header_h) } - else if let Some(scene_area) = scene_area { scene_area.clip_w(self.scenes_w) } - else { area.clip_w(self.scenes_w).clip_h(self.header_h) }, &CORNERS)? - }) -})); - -struct ArrangerVHeader<'a> { - tracks: &'a Vec, - cols: Vec<(usize, usize)>, - focused: bool, - selected: ArrangerSelection, - scenes_w: u16, - header_h: u16, - timebase: &'a Arc, - current: &'a Arc, -} -impl<'a> From<&'a ArrangerTui> for ArrangerVHeader<'a> { - fn from (state: &'a ArrangerTui) -> Self { - Self { - tracks: &state.tracks, - cols: track_widths(state.tracks()), - focused: true, - selected: state.selected, - scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, - header_h: 3, - timebase: state.clock().timebase(), - current: &state.clock().playhead, - } - } -} -render!(|self: ArrangerVHeader<'a>|row!( - (track, w) in self.tracks.iter().zip(self.cols.iter().map(|col|col.0)) => { - // name and width of track - let name = track.name().read().unwrap(); - let max_w = w.saturating_sub(1).min(name.len()).max(2); - let name = format!("▎{}", &name[0..max_w]); - let name = Tui::bold(true, Tui::fg(track.color.lightest.rgb, name)); - // beats elapsed - let elapsed = if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() { - let length = phrase.read().unwrap().length; - let elapsed = track.player.pulses_since_start().unwrap(); - let elapsed = self.timebase.format_beats_1_short( - (elapsed as usize % length) as f64 - ); - format!("▎+{elapsed:>}") - } else { - String::from("▎") - }; - // beats until switchover - let until_next = track.player.next_phrase().as_ref().map(|(t, _)|{ - let target = t.pulse.get(); - let current = self.current.pulse.get(); - if target > current { - let remaining = target - current; - format!("▎-{:>}", self.timebase.format_beats_0_short(remaining)) - } else { - String::new() - } - }).unwrap_or(String::from("▎")); - let timer = col!([until_next, elapsed]); - // name of active MIDI input - let _input = format!("▎>{}", track.player.midi_ins().get(0) - .map(|port|port.short_name()) - .transpose()? - .unwrap_or("(none)".into())); - // name of active MIDI output - let _output = format!("▎<{}", track.player.midi_outs().get(0) - .map(|port|port.short_name()) - .transpose()? - .unwrap_or("(none)".into())); - Tui::push_x(self.scenes_w, - Tui::bg(track.color().base.rgb, - Tui::min_xy(w as u16, self.header_h, - col!([name, timer])))) - } -)); - -struct ArrangerVContent<'a> { - size: &'a Measure, - scenes: &'a Vec, - tracks: &'a Vec, - rows: Vec<(usize, usize)>, - cols: Vec<(usize, usize)>, - header_h: u16, -} -impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVContent<'a> { - fn from ((state, factor): (&'a ArrangerTui, usize)) -> Self { - Self { - size: &state.size, - scenes: &state.scenes, - tracks: &state.tracks, - rows: ArrangerScene::ppqs(state.scenes(), factor), - cols: track_widths(state.tracks()), - header_h: 3, - } - } -} -render!(|self: ArrangerVContent<'a>|Fixed::h( - (self.size.h() as u16).saturating_sub(self.header_h), - col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => { - let height = 1.max((pulses / PPQ) as u16); - let playing = scene.is_playing(self.tracks); - Fixed::h(height, row!([ - if playing { "▶ " } else { " " }, - Tui::bold(true, scene.name.read().unwrap().as_str()), - row!((track, w) in self.cols.iter().map(|col|col.0).enumerate() => { - Fixed::wh(w as u16, height, Layers::new(move |add|{ - let mut bg = TuiTheme::border_bg(); - match (self.tracks.get(track), scene.clips.get(track)) { - (Some(track), Some(Some(phrase))) => { - let name = &(phrase as &Arc>).read().unwrap().name; - let name = format!("{}", name); - let max_w = name.len().min((w as usize).saturating_sub(2)); - let color = phrase.read().unwrap().color; - bg = color.dark.rgb; - if let Some((_, Some(ref playing))) = track.player.play_phrase() { - if *playing.read().unwrap() == *phrase.read().unwrap() { - bg = color.light.rgb - } - }; - add(&Fixed::w(w as u16, Tui::push_x(1, &name.as_str()[0..max_w])))?; - }, - _ => {} - }; - //add(&Background(bg)) - Ok(()) - })) - })]) - ) - }) -)); - -impl HasScenes for ArrangerTui { - fn scenes (&self) -> &Vec { - &self.scenes - } - fn scenes_mut (&mut self) -> &mut Vec { - &mut self.scenes - } - fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerScene> - { - let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string()); - let scene = ArrangerScene { - name: Arc::new(name.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 selected_scene (&self) -> Option<&ArrangerScene> { - self.selected.scene().map(|s|self.scenes().get(s)).flatten() - } - fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { - self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten() - } -} - -#[derive(Default, Debug, Clone)] 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 ArrangerSceneApi for ArrangerScene { - fn name (&self) -> &Arc> { - &self.name - } - fn clips (&self) -> &Vec>>> { - &self.clips - } - fn color (&self) -> ItemPalette { - self.color - } -} -impl HasTracks for ArrangerTui { - fn tracks (&self) -> &Vec { - &self.tracks - } - fn tracks_mut (&mut self) -> &mut Vec { - &mut self.tracks - } -} -impl ArrangerTracksApi for ArrangerTui { - fn track_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerTrack> - { - let name = name.map_or_else(||self.track_default_name(), |x|x.to_string()); - let track = ArrangerTrack { - width: name.len() + 2, - name: Arc::new(name.into()), - color: color.unwrap_or_else(||ItemPalette::random()), - player: PhrasePlayerModel::from(&self.clock), - }; - self.tracks_mut().push(track); - let index = self.tracks().len() - 1; - Ok(&mut self.tracks_mut()[index]) - } - fn track_del (&mut self, index: usize) { - self.tracks_mut().remove(index); - for scene in self.scenes_mut().iter_mut() { - scene.clips.remove(index); - } - } -} - -#[derive(Debug)] pub struct ArrangerTrack { - /// Name of track - pub(crate) name: Arc>, - /// Preferred width of track column - pub(crate) width: usize, - /// Identifying color of track - pub(crate) color: ItemPalette, - /// MIDI player state - pub(crate) player: PhrasePlayerModel, -} -has_clock!(|self:ArrangerTrack|self.player.clock()); -has_player!(|self:ArrangerTrack|self.player); -impl ArrangerTrackApi for ArrangerTrack { - /// Name of track - fn name (&self) -> &Arc> { - &self.name - } - /// Preferred width of track column - fn width (&self) -> usize { - self.width - } - /// Preferred width of track column - fn width_mut (&mut self) -> &mut usize { - &mut self.width - } - /// Identifying color of track - fn color (&self) -> ItemPalette { - self.color - } -} -#[derive(PartialEq, Clone, Copy, Debug)] -/// Represents the current user selection in the arranger -pub enum ArrangerSelection { - /// 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 ArrangerSelection { - pub fn description ( - &self, - tracks: &Vec, - scenes: &Vec, - ) -> String { - format!("Selected: {}", match self { - Self::Mix => format!("Everything"), - Self::Track(t) => match tracks.get(*t) { - Some(track) => format!("T{t}: {}", &track.name.read().unwrap()), - None => format!("T??"), - }, - Self::Scene(s) => match scenes.get(*s) { - Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()), - None => format!("S??"), - }, - 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"), - } - }) - } - 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 { - 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 - } - } -} - -#[derive(Clone, Debug)] -pub enum ArrangerClipCommand { - Play, - Get(usize, usize), - Set(usize, usize, Option>>), - Edit(Option>>), - SetLoop(bool), - RandomColor, -} //impl Command for ArrangerClipCommand { //fn execute (self, state: &mut T) -> Perhaps { @@ -890,12 +287,6 @@ pub enum ArrangerClipCommand { //Ok(None) //} //} - -pub fn arranger_content_horizontal ( - view: &ArrangerTui, -) -> impl Render + use<'_> { - todo!() -} //let focused = true; //let _tracks = view.tracks(); //lay!( diff --git a/crates/tek/src/tui/arranger_clip.rs b/crates/tek/src/tui/arranger_clip.rs new file mode 100644 index 00000000..040db3aa --- /dev/null +++ b/crates/tek/src/tui/arranger_clip.rs @@ -0,0 +1,23 @@ +use crate::*; + +pub fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option { + use KeyCode::{Char, Up, Down, Left, Right, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + use ArrangerClipCommand as Clip; + Some(match input.event() { + key_pat!(Char('w')) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }), + key_pat!(Char('s')) => Cmd::Select(Select::Clip(t, s + 1)), + key_pat!(Char('a')) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }), + key_pat!(Char('d')) => Cmd::Select(Select::Clip(t + 1, s)), + key_pat!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)), + key_pat!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)), + key_pat!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)), + key_pat!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)), + key_pat!(Delete) => Cmd::Clip(Clip::Set(t, s, None)), + //key_pat!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemPalette::random())), + //key_pat!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))), + //key_pat!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))), + _ => return None + }) +} diff --git a/crates/tek/src/tui/arranger_mode_h.rs b/crates/tek/src/tui/arranger_mode_h.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/tek/src/tui/arranger_mode_v.rs b/crates/tek/src/tui/arranger_mode_v.rs new file mode 100644 index 00000000..227d982f --- /dev/null +++ b/crates/tek/src/tui/arranger_mode_v.rs @@ -0,0 +1,274 @@ +use crate::*; + +pub struct ArrangerVColumnSeparator { + cols: Vec<(usize, usize)>, + scenes_w: u16, + sep_fg: Color, +} + +impl From<&ArrangerTui> for ArrangerVColumnSeparator { + fn from (state: &ArrangerTui) -> Self { + Self { + cols: track_widths(state.tracks()), + scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, + sep_fg: TuiTheme::separator_fg(false), + } + } +} + +render!(|self: ArrangerVColumnSeparator|render(move|to: &mut TuiOutput|{ + let style = Some(Style::default().fg(self.sep_fg)); + Ok(for x in self.cols.iter().map(|col|col.1) { + let x = self.scenes_w + to.area().x() + x as u16; + for y in to.area().y()..to.area().y2() { + to.blit(&"▎", x, y, style); + } + }) +})); + +pub struct ArrangerVRowSeparator { + rows: Vec<(usize, usize)>, + sep_fg: Color, +} + +impl From<(&ArrangerTui, usize)> for ArrangerVRowSeparator { + fn from ((state, factor): (&ArrangerTui, usize)) -> Self { + Self { + rows: ArrangerScene::ppqs(state.scenes(), factor), + sep_fg: TuiTheme::separator_fg(false), + } + } +} + +render!(|self: ArrangerVRowSeparator|render(move|to: &mut TuiOutput|{ + Ok(for y in self.rows.iter().map(|row|row.1) { + let y = to.area().y() + (y / PPQ) as u16 + 1; + if y >= to.buffer.area.height { break } + for x in to.area().x()..to.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 = self.sep_fg; + } + } + }) +})); + +pub struct ArrangerVCursor { + cols: Vec<(usize, usize)>, + rows: Vec<(usize, usize)>, + focused: bool, + selected: ArrangerSelection, + scenes_w: u16, + header_h: u16, +} + +impl From<(&ArrangerTui, usize)> for ArrangerVCursor { + fn from ((state, factor): (&ArrangerTui, usize)) -> Self { + Self { + cols: track_widths(state.tracks()), + rows: ArrangerScene::ppqs(state.scenes(), factor), + focused: true, + selected: state.selected, + scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, + header_h: 3, + } + } +} + +render!(|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{ + let area = to.area(); + let focused = self.focused; + let selected = self.selected; + let get_track_area = |t: usize| [ + self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(), + self.cols[t].0 as u16, area.h(), + ]; + let get_scene_area = |s: usize| [ + area.x(), self.header_h + area.y() + (self.rows[s].1 / PPQ) as u16, + area.w(), (self.rows[s].0 / PPQ) as u16 + ]; + let get_clip_area = |t: usize, s: usize| [ + self.scenes_w + area.x() + self.cols[t].1 as u16, + self.header_h + area.y() + (self.rows[s].1/PPQ) as u16, + self.cols[t].0 as u16, + (self.rows[s].0 / PPQ) 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 { + ArrangerSelection::Mix => area, + ArrangerSelection::Track(t) => { + track_area = Some(get_track_area(t)); + area + }, + ArrangerSelection::Scene(s) => { + scene_area = Some(get_scene_area(s)); + area + }, + ArrangerSelection::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 + }, + }; + let bg = TuiTheme::border_bg(); + if let Some([x, y, width, height]) = track_area { + to.fill_fg([x, y, 1, height], bg); + to.fill_fg([x + width, y, 1, height], bg); + } + if let Some([_, y, _, height]) = scene_area { + to.fill_ul([area.x(), y - 1, area.w(), 1], bg); + to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg); + } + Ok(if focused { + to.render_in(if let Some(clip_area) = clip_area { clip_area } + else if let Some(track_area) = track_area { track_area.clip_h(self.header_h) } + else if let Some(scene_area) = scene_area { scene_area.clip_w(self.scenes_w) } + else { area.clip_w(self.scenes_w).clip_h(self.header_h) }, &CORNERS)? + }) +})); + +pub struct ArrangerVHeader<'a> { + tracks: &'a Vec, + cols: Vec<(usize, usize)>, + focused: bool, + selected: ArrangerSelection, + scenes_w: u16, + header_h: u16, + timebase: &'a Arc, + current: &'a Arc, +} + +impl<'a> From<&'a ArrangerTui> for ArrangerVHeader<'a> { + fn from (state: &'a ArrangerTui) -> Self { + Self { + tracks: &state.tracks, + cols: track_widths(state.tracks()), + focused: true, + selected: state.selected, + scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, + header_h: 3, + timebase: state.clock().timebase(), + current: &state.clock().playhead, + } + } +} + +render!(|self: ArrangerVHeader<'a>|row!( + (track, w) in self.tracks.iter().zip(self.cols.iter().map(|col|col.0)) => { + // name and width of track + let name = track.name().read().unwrap(); + let max_w = w.saturating_sub(1).min(name.len()).max(2); + let name = format!("▎{}", &name[0..max_w]); + let name = Tui::bold(true, Tui::fg(track.color.lightest.rgb, name)); + // beats elapsed + let elapsed = if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() { + let length = phrase.read().unwrap().length; + let elapsed = track.player.pulses_since_start().unwrap(); + let elapsed = self.timebase.format_beats_1_short( + (elapsed as usize % length) as f64 + ); + format!("▎+{elapsed:>}") + } else { + String::from("▎") + }; + // beats until switchover + let until_next = track.player.next_phrase().as_ref().map(|(t, _)|{ + let target = t.pulse.get(); + let current = self.current.pulse.get(); + if target > current { + let remaining = target - current; + format!("▎-{:>}", self.timebase.format_beats_0_short(remaining)) + } else { + String::new() + } + }).unwrap_or(String::from("▎")); + let timer = col!([until_next, elapsed]); + // name of active MIDI input + let _input = format!("▎>{}", track.player.midi_ins().get(0) + .map(|port|port.short_name()) + .transpose()? + .unwrap_or("(none)".into())); + // name of active MIDI output + let _output = format!("▎<{}", track.player.midi_outs().get(0) + .map(|port|port.short_name()) + .transpose()? + .unwrap_or("(none)".into())); + Tui::push_x(self.scenes_w, + Tui::bg(track.color().base.rgb, + Tui::min_xy(w as u16, self.header_h, + col!([name, timer])))) + } +)); + +pub struct ArrangerVContent<'a> { + size: &'a Measure, + scenes: &'a Vec, + tracks: &'a Vec, + rows: Vec<(usize, usize)>, + cols: Vec<(usize, usize)>, + header_h: u16, +} + +impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVContent<'a> { + fn from ((state, factor): (&'a ArrangerTui, usize)) -> Self { + Self { + size: &state.size, + scenes: &state.scenes, + tracks: &state.tracks, + rows: ArrangerScene::ppqs(state.scenes(), factor), + cols: track_widths(state.tracks()), + header_h: 3, + } + } +} + +render!(|self: ArrangerVContent<'a>|Fixed::h( + (self.size.h() as u16).saturating_sub(self.header_h), + col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => { + let height = 1.max((pulses / PPQ) as u16); + let playing = scene.is_playing(self.tracks); + Fixed::h(height, row!([ + if playing { "▶ " } else { " " }, + Tui::bold(true, scene.name.read().unwrap().as_str()), + row!((track, w) in self.cols.iter().map(|col|col.0).enumerate() => { + Fixed::wh(w as u16, height, Layers::new(move |add|{ + let mut bg = TuiTheme::border_bg(); + match (self.tracks.get(track), scene.clips.get(track)) { + (Some(track), Some(Some(phrase))) => { + let name = &(phrase as &Arc>).read().unwrap().name; + let name = format!("{}", name); + let max_w = name.len().min((w as usize).saturating_sub(2)); + let color = phrase.read().unwrap().color; + bg = color.dark.rgb; + if let Some((_, Some(ref playing))) = track.player.play_phrase() { + if *playing.read().unwrap() == *phrase.read().unwrap() { + bg = color.light.rgb + } + }; + add(&Fixed::w(w as u16, Tui::push_x(1, &name.as_str()[0..max_w])))?; + }, + _ => {} + }; + //add(&Background(bg)) + Ok(()) + })) + })]) + ) + }) +)); + +fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { + let mut widths = vec![]; + let mut total = 0; + for track in tracks.iter() { + let width = track.width; + widths.push((width, total)); + total += width; + } + widths.push((0, total)); + widths +} diff --git a/crates/tek/src/tui/arranger_scene.rs b/crates/tek/src/tui/arranger_scene.rs new file mode 100644 index 00000000..4f0139be --- /dev/null +++ b/crates/tek/src/tui/arranger_scene.rs @@ -0,0 +1,66 @@ +use crate::*; +pub fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { + use KeyCode::{Char, Up, Down, Right, Enter, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + use ArrangerSceneCommand as Scene; + Some(match input.event() { + key_pat!(Char('w')) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }), + key_pat!(Char('s')) => Cmd::Select(Select::Scene(s + 1)), + key_pat!(Char('d')) => Cmd::Select(Select::Clip(0, s)), + key_pat!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)), + key_pat!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)), + key_pat!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)), + key_pat!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)), + key_pat!(Char('q')) => Cmd::Scene(Scene::Play(s)), + key_pat!(Delete) => Cmd::Scene(Scene::Delete(s)), + //key_pat!(Char('c')) => Cmd::Track(Scene::Color(s, ItemPalette::random())), + _ => return None + }) +} +impl HasScenes for ArrangerTui { + fn scenes (&self) -> &Vec { + &self.scenes + } + fn scenes_mut (&mut self) -> &mut Vec { + &mut self.scenes + } + fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerScene> + { + let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string()); + let scene = ArrangerScene { + name: Arc::new(name.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 selected_scene (&self) -> Option<&ArrangerScene> { + self.selected.scene().map(|s|self.scenes().get(s)).flatten() + } + fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { + self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten() + } +} +#[derive(Default, Debug, Clone)] 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 ArrangerSceneApi for ArrangerScene { + fn name (&self) -> &Arc> { + &self.name + } + fn clips (&self) -> &Vec>>> { + &self.clips + } + fn color (&self) -> ItemPalette { + self.color + } +} diff --git a/crates/tek/src/tui/arranger_select.rs b/crates/tek/src/tui/arranger_select.rs new file mode 100644 index 00000000..0718e7e9 --- /dev/null +++ b/crates/tek/src/tui/arranger_select.rs @@ -0,0 +1,70 @@ +use crate::*; + +#[derive(PartialEq, Clone, Copy, Debug)] +/// Represents the current user selection in the arranger +pub enum ArrangerSelection { + /// 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 ArrangerSelection { + pub fn description ( + &self, + tracks: &Vec, + scenes: &Vec, + ) -> String { + format!("Selected: {}", match self { + Self::Mix => format!("Everything"), + Self::Track(t) => match tracks.get(*t) { + Some(track) => format!("T{t}: {}", &track.name.read().unwrap()), + None => format!("T??"), + }, + Self::Scene(s) => match scenes.get(*s) { + Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()), + None => format!("S??"), + }, + 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"), + } + }) + } + 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 { + 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 + } + } +} diff --git a/crates/tek/src/tui/arranger_track.rs b/crates/tek/src/tui/arranger_track.rs new file mode 100644 index 00000000..74077109 --- /dev/null +++ b/crates/tek/src/tui/arranger_track.rs @@ -0,0 +1,80 @@ +use crate::*; +pub fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option { + use KeyCode::{Char, Down, Left, Right, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + use ArrangerTrackCommand as Track; + Some(match input.event() { + key_pat!(Char('s')) => Cmd::Select(Select::Clip(t, 0)), + key_pat!(Char('a')) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }), + key_pat!(Char('d')) => Cmd::Select(Select::Track(t + 1)), + key_pat!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)), + key_pat!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)), + key_pat!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)), + key_pat!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)), + key_pat!(Delete) => Cmd::Track(Track::Delete(t)), + //key_pat!(Char('c')) => Cmd::Track(Track::Color(t, ItemPalette::random())), + _ => return None + }) +} +impl HasTracks for ArrangerTui { + fn tracks (&self) -> &Vec { + &self.tracks + } + fn tracks_mut (&mut self) -> &mut Vec { + &mut self.tracks + } +} +impl ArrangerTracksApi for ArrangerTui { + fn track_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerTrack> + { + let name = name.map_or_else(||self.track_default_name(), |x|x.to_string()); + let track = ArrangerTrack { + width: name.len() + 2, + name: Arc::new(name.into()), + color: color.unwrap_or_else(||ItemPalette::random()), + player: PhrasePlayerModel::from(&self.clock), + }; + self.tracks_mut().push(track); + let index = self.tracks().len() - 1; + Ok(&mut self.tracks_mut()[index]) + } + fn track_del (&mut self, index: usize) { + self.tracks_mut().remove(index); + for scene in self.scenes_mut().iter_mut() { + scene.clips.remove(index); + } + } +} + +#[derive(Debug)] pub struct ArrangerTrack { + /// Name of track + pub(crate) name: Arc>, + /// Preferred width of track column + pub(crate) width: usize, + /// Identifying color of track + pub(crate) color: ItemPalette, + /// MIDI player state + pub(crate) player: PhrasePlayerModel, +} +has_clock!(|self:ArrangerTrack|self.player.clock()); +has_player!(|self:ArrangerTrack|self.player); +impl ArrangerTrackApi for ArrangerTrack { + /// Name of track + fn name (&self) -> &Arc> { + &self.name + } + /// Preferred width of track column + fn width (&self) -> usize { + self.width + } + /// Preferred width of track column + fn width_mut (&mut self) -> &mut usize { + &mut self.width + } + /// Identifying color of track + fn color (&self) -> ItemPalette { + self.color + } +} diff --git a/crates/tek/src/tui/status_bar.rs b/crates/tek/src/tui/status_bar.rs index 1835f563..05f47c3e 100644 --- a/crates/tek/src/tui/status_bar.rs +++ b/crates/tek/src/tui/status_bar.rs @@ -86,112 +86,125 @@ render!(|self: SequencerStatus|Fixed::h(2, lay!([ })), ]))); -impl Bar for ArrangerStatus { - type State = (ArrangerFocus, ArrangerSelection, bool); - fn hotkey_fg () -> Color where Self: Sized { - TuiTheme::HOTKEY_FG - } - fn update (&mut self, (focused, selected, entered): &Self::State) { - *self = match focused { - //ArrangerFocus::Menu => { todo!() }, - ArrangerFocus::Transport(_) => ArrangerStatus::Transport, - ArrangerFocus::Arranger => match selected { - ArrangerSelection::Mix => ArrangerStatus::ArrangerMix, - ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack, - ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene, - ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip, - }, - ArrangerFocus::Phrases => ArrangerStatus::PhrasePool, - ArrangerFocus::PhraseEditor => match entered { - true => ArrangerStatus::PhraseEdit, - false => ArrangerStatus::PhraseView, - }, - } - } +//impl Bar for ArrangerStatus { + //type State = (ArrangerFocus, ArrangerSelection, bool); + //fn hotkey_fg () -> Color where Self: Sized { + //TuiTheme::HOTKEY_FG + //} + //fn update (&mut self, (focused, selected, entered): &Self::State) { + //*self = match focused { + ////ArrangerFocus::Menu => { todo!() }, + //ArrangerFocus::Transport(_) => ArrangerStatus::Transport, + //ArrangerFocus::Arranger => match selected { + //ArrangerSelection::Mix => ArrangerStatus::ArrangerMix, + //ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack, + //ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene, + //ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip, + //}, + //ArrangerFocus::Phrases => ArrangerStatus::PhrasePool, + //ArrangerFocus::PhraseEditor => match entered { + //true => ArrangerStatus::PhraseEdit, + //false => ArrangerStatus::PhraseView, + //}, + //} + //} +//} + +//render!(|self: ArrangerStatus|{ + + //let label = match self { + //Self::Transport => "TRANSPORT", + //Self::ArrangerMix => "PROJECT", + //Self::ArrangerTrack => "TRACK", + //Self::ArrangerScene => "SCENE", + //Self::ArrangerClip => "CLIP", + //Self::PhrasePool => "SEQ LIST", + //Self::PhraseView => "VIEW SEQ", + //Self::PhraseEdit => "EDIT SEQ", + //}; + + //let status_bar_bg = TuiTheme::status_bar_bg(); + + //let mode_bg = TuiTheme::mode_bg(); + //let mode_fg = TuiTheme::mode_fg(); + //let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} ")))); + + //let commands = match self { + //Self::ArrangerMix => Self::command(&[ + //["", "c", "olor"], + //["", "<>", "resize"], + //["", "+-", "zoom"], + //["", "n", "ame/number"], + //["", "Enter", " stop all"], + //]), + //Self::ArrangerClip => Self::command(&[ + //["", "g", "et"], + //["", "s", "et"], + //["", "a", "dd"], + //["", "i", "ns"], + //["", "d", "up"], + //["", "e", "dit"], + //["", "c", "olor"], + //["re", "n", "ame"], + //["", ",.", "select"], + //["", "Enter", " launch"], + //]), + //Self::ArrangerTrack => Self::command(&[ + //["re", "n", "ame"], + //["", ",.", "resize"], + //["", "<>", "move"], + //["", "i", "nput"], + //["", "o", "utput"], + //["", "m", "ute"], + //["", "s", "olo"], + //["", "Del", "ete"], + //["", "Enter", " stop"], + //]), + //Self::ArrangerScene => Self::command(&[ + //["re", "n", "ame"], + //["", "Del", "ete"], + //["", "Enter", " launch"], + //]), + //Self::PhrasePool => Self::command(&[ + //["", "a", "ppend"], + //["", "i", "nsert"], + //["", "d", "uplicate"], + //["", "Del", "ete"], + //["", "c", "olor"], + //["re", "n", "ame"], + //["leng", "t", "h"], + //["", ",.", "move"], + //["", "+-", "resize view"], + //]), + //Self::PhraseView => Self::command(&[ + //["", "enter", " edit"], + //["", "arrows/pgup/pgdn", " scroll"], + //["", "+=", "zoom"], + //]), + //Self::PhraseEdit => Self::command(&[ + //["", "esc", " exit"], + //["", "a", "ppend"], + //["", "s", "et"], + //["", "][", "length"], + //["", "+-", "zoom"], + //]), + //_ => Self::command(&[]) + //}; + + ////let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}")); + //Tui::bg(status_bar_bg, Fill::w(row!([mode, commands]))) + +//}); + +/// Status bar for arranger app +#[derive(Copy, Clone, Debug)] +pub enum ArrangerStatus { + Transport, + ArrangerMix, + ArrangerTrack, + ArrangerScene, + ArrangerClip, + PhrasePool, + PhraseView, + PhraseEdit, } - -render!(|self: ArrangerStatus|{ - - let label = match self { - Self::Transport => "TRANSPORT", - Self::ArrangerMix => "PROJECT", - Self::ArrangerTrack => "TRACK", - Self::ArrangerScene => "SCENE", - Self::ArrangerClip => "CLIP", - Self::PhrasePool => "SEQ LIST", - Self::PhraseView => "VIEW SEQ", - Self::PhraseEdit => "EDIT SEQ", - }; - - let status_bar_bg = TuiTheme::status_bar_bg(); - - let mode_bg = TuiTheme::mode_bg(); - let mode_fg = TuiTheme::mode_fg(); - let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} ")))); - - let commands = match self { - Self::ArrangerMix => Self::command(&[ - ["", "c", "olor"], - ["", "<>", "resize"], - ["", "+-", "zoom"], - ["", "n", "ame/number"], - ["", "Enter", " stop all"], - ]), - Self::ArrangerClip => Self::command(&[ - ["", "g", "et"], - ["", "s", "et"], - ["", "a", "dd"], - ["", "i", "ns"], - ["", "d", "up"], - ["", "e", "dit"], - ["", "c", "olor"], - ["re", "n", "ame"], - ["", ",.", "select"], - ["", "Enter", " launch"], - ]), - Self::ArrangerTrack => Self::command(&[ - ["re", "n", "ame"], - ["", ",.", "resize"], - ["", "<>", "move"], - ["", "i", "nput"], - ["", "o", "utput"], - ["", "m", "ute"], - ["", "s", "olo"], - ["", "Del", "ete"], - ["", "Enter", " stop"], - ]), - Self::ArrangerScene => Self::command(&[ - ["re", "n", "ame"], - ["", "Del", "ete"], - ["", "Enter", " launch"], - ]), - Self::PhrasePool => Self::command(&[ - ["", "a", "ppend"], - ["", "i", "nsert"], - ["", "d", "uplicate"], - ["", "Del", "ete"], - ["", "c", "olor"], - ["re", "n", "ame"], - ["leng", "t", "h"], - ["", ",.", "move"], - ["", "+-", "resize view"], - ]), - Self::PhraseView => Self::command(&[ - ["", "enter", " edit"], - ["", "arrows/pgup/pgdn", " scroll"], - ["", "+=", "zoom"], - ]), - Self::PhraseEdit => Self::command(&[ - ["", "esc", " exit"], - ["", "a", "ppend"], - ["", "s", "et"], - ["", "][", "length"], - ["", "+-", "zoom"], - ]), - _ => Self::command(&[]) - }; - - //let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}")); - Tui::bg(status_bar_bg, Fill::w(row!([mode, commands]))) - -}); From de77daf565a49a385ee66044ff7f998867807b17 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 18:36:20 +0100 Subject: [PATCH 029/905] start porting some sequencer keybinds --- crates/tek/src/midi.rs | 2 - crates/tek/src/midi/midi_scene.rs | 96 ----------------------- crates/tek/src/midi/midi_track.rs | 87 --------------------- crates/tek/src/tui/app_arranger.rs | 80 ++++++++++++------- crates/tek/src/tui/arranger_scene.rs | 96 +++++++++++++++++++++++ crates/tek/src/tui/arranger_track.rs | 110 ++++++++++++++++++++++++--- 6 files changed, 248 insertions(+), 223 deletions(-) delete mode 100644 crates/tek/src/midi/midi_scene.rs delete mode 100644 crates/tek/src/midi/midi_track.rs diff --git a/crates/tek/src/midi.rs b/crates/tek/src/midi.rs index 240d102f..9e8ff123 100644 --- a/crates/tek/src/midi.rs +++ b/crates/tek/src/midi.rs @@ -7,8 +7,6 @@ pub(crate) mod midi_out; pub(crate) use midi_out::*; pub(crate) mod midi_phrase; pub(crate) use midi_phrase::*; pub(crate) mod midi_play; pub(crate) use midi_play::*; pub(crate) mod midi_rec; pub(crate) use midi_rec::*; -pub(crate) mod midi_scene; pub(crate) use midi_scene::*; -pub(crate) mod midi_track; pub(crate) use midi_track::*; /// Add "all notes off" to the start of a buffer. pub fn all_notes_off (output: &mut [Vec>]) { diff --git a/crates/tek/src/midi/midi_scene.rs b/crates/tek/src/midi/midi_scene.rs deleted file mode 100644 index fe0825dc..00000000 --- a/crates/tek/src/midi/midi_scene.rs +++ /dev/null @@ -1,96 +0,0 @@ -use crate::*; - -pub trait HasScenes { - fn scenes (&self) -> &Vec; - fn scenes_mut (&mut self) -> &mut Vec; - fn scene_add (&mut self, name: Option<&str>, color: Option) -> Usually<&mut S>; - fn scene_del (&mut self, index: usize) { - self.scenes_mut().remove(index); - } - fn scene_default_name (&self) -> String { - format!("Scene {}", self.scenes().len() + 1) - } - fn selected_scene (&self) -> Option<&S> { - None - } - fn selected_scene_mut (&mut self) -> Option<&mut S> { - None - } -} - -#[derive(Clone, Debug)] -pub enum ArrangerSceneCommand { - Add, - Delete(usize), - RandomColor, - Play(usize), - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), -} - -//impl Command for ArrangerSceneCommand { - //fn execute (self, state: &mut T) -> Perhaps { - //match self { - //Self::Delete(index) => { state.scene_del(index); }, - //_ => todo!() - //} - //Ok(None) - //} -//} - -pub trait ArrangerSceneApi: Sized { - fn name (&self) -> &Arc>; - fn clips (&self) -> &Vec>>>; - fn color (&self) -> ItemPalette; - - fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> { - let mut total = 0; - if factor == 0 { - scenes.iter().map(|scene|{ - let pulses = scene.pulses().max(PPQ); - total = total + pulses; - (pulses, total - pulses) - }).collect() - } else { - (0..=scenes.len()).map(|i|{ - (factor*PPQ, factor*PPQ*i) - }).collect() - } - } - - fn longest_name (scenes: &[Self]) -> usize { - scenes.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) - } - - /// Returns the pulse length of the longest phrase in the scene - 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 phrases in the scene are - /// currently playing on the given collection of tracks. - fn is_playing (&self, tracks: &[T]) -> bool { - self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate() - .all(|(track_index, clip)|match clip { - Some(clip) => tracks - .get(track_index) - .map(|track|{ - if let Some((_, Some(phrase))) = track.player().play_phrase() { - *phrase.read().unwrap() == *clip.read().unwrap() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) - } - - fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None } - } - -} diff --git a/crates/tek/src/midi/midi_track.rs b/crates/tek/src/midi/midi_track.rs deleted file mode 100644 index 0a4fc7e6..00000000 --- a/crates/tek/src/midi/midi_track.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::*; - -pub trait HasTracks: Send + Sync { - fn tracks (&self) -> &Vec; - fn tracks_mut (&mut self) -> &mut Vec; -} - -impl HasTracks for Vec { - fn tracks (&self) -> &Vec { - self - } - fn tracks_mut (&mut self) -> &mut Vec { - self - } -} - -pub trait ArrangerTracksApi: HasTracks { - fn track_add (&mut self, name: Option<&str>, color: Option)-> Usually<&mut T>; - fn track_del (&mut self, index: usize); - fn track_default_name (&self) -> String { - format!("Track {}", self.tracks().len() + 1) - } -} - -#[derive(Clone, Debug)] -pub enum ArrangerTrackCommand { - Add, - Delete(usize), - RandomColor, - Stop, - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), -} - -pub trait ArrangerTrackApi: HasPlayer + Send + Sync + Sized { - /// Name of track - fn name (&self) -> &Arc>; - /// Preferred width of track column - fn width (&self) -> usize; - /// Preferred width of track column - fn width_mut (&mut self) -> &mut usize; - /// Identifying color of track - fn color (&self) -> ItemPalette; - - fn longest_name (tracks: &[Self]) -> usize { - tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) - } - - const MIN_WIDTH: usize = 3; - - fn width_inc (&mut self) { - *self.width_mut() += 1; - } - - fn width_dec (&mut self) { - if self.width() > Self::MIN_WIDTH { - *self.width_mut() -= 1; - } - } -} - -/// Hosts the JACK callback for a collection of tracks -pub struct TracksAudio<'a, T: ArrangerTrackApi, H: HasTracks>( - // Track collection - pub &'a mut H, - /// Note buffer - pub &'a mut Vec, - /// Note chunk buffer - pub &'a mut Vec>>, - /// Marker - pub PhantomData, -); - -impl<'a, T: ArrangerTrackApi, H: HasTracks> Audio for TracksAudio<'a, T, H> { - #[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.tracks_mut().iter_mut() { - if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit { - return Control::Quit - } - } - Control::Continue - } -} diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 20c27cd4..bd05a5f9 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -1,4 +1,6 @@ use crate::*; +use ClockCommand::{Play, Pause}; +use KeyCode::{Char, Down, Right, Delete}; /// Root view for standalone `tek_arranger` pub struct ArrangerTui { pub jack: Arc>, @@ -42,10 +44,6 @@ from_jack!(|jack| ArrangerTui Self { note_buf: vec![], perf: PerfModel::default(), }); -has_clock!(|self: ArrangerTui|&self.clock); -has_phrases!(|self: ArrangerTui|self.phrases.phrases); -has_editor!(|self: ArrangerTui|self.editor); -handle!(|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input)); render!(|self: ArrangerTui|{ let arranger = ||lay!(|add|{ let color = self.color; @@ -112,10 +110,15 @@ audio!(|self: ArrangerTui, client, scope|{ self.perf.update(t0, scope); return Control::Continue }); +has_clock!(|self: ArrangerTui|&self.clock); +has_phrases!(|self: ArrangerTui|self.phrases.phrases); +has_editor!(|self: ArrangerTui|self.editor); +handle!(|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input)); #[derive(Clone, Debug)] pub enum ArrangerCommand { Undo, Redo, Clear, + StopAll, Color(ItemPalette), Clock(ClockCommand), Scene(ArrangerSceneCommand), @@ -126,35 +129,60 @@ audio!(|self: ArrangerTui, client, scope|{ Phrases(PhrasesCommand), Editor(PhraseCommand), } -input_to_command!(ArrangerCommand: |state:ArrangerTui,input|to_arranger_command(state, input)?); -fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option { - use ArrangerSelection::*; - use ArrangerCommand as Cmd; - use KeyCode::Char; +input_to_command!(ArrangerCommand: |state:ArrangerTui,input|{ + use ArrangerSelection as Selected; + use ArrangerCommand::*; // WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor - Some(match input.event() { - key_pat!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))), - key_pat!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)), - key_pat!(Char('+')) => Cmd::Zoom(0), // TODO - key_pat!(Char('=')) => Cmd::Zoom(0), // TODO - key_pat!(Char('_')) => Cmd::Zoom(0), // TODO - key_pat!(Char('-')) => Cmd::Zoom(0), // TODO - key_pat!(Char('`')) => { todo!("toggle state mode") }, - key_pat!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add), - key_pat!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add), + match input.event() { + // Transport: Play/pause + key_pat!(Char(' ')) => + Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), + // Transport: Play from start or rewind to start + key_pat!(Shift-Char(' ')) => + Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), + // TODO: u: undo + key_pat!(Char('u')) => + { todo!("undo") }, + // TODO: Shift-U: redo + key_pat!(Char('U')) => + { todo!("redo") }, + // TODO: k: toggle on-screen keyboard + key_pat!(Ctrl-Char('k')) => + { todo!("keyboard") }, + key_pat!(Char('e')) => + Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))), + key_pat!(Char('l')) => + Clip(ArrangerClipCommand::SetLoop(false)), + key_pat!(Ctrl-Char('a')) => + Scene(ArrangerSceneCommand::Add), + key_pat!(Ctrl-Char('t')) => + Track(ArrangerTrackCommand::Add), + key_pat!(Char('0')) => match state.selected() { + Selected::Mix => StopAll, + Selected::Track(t) => return None, + Selected::Scene(s) => return None, + Selected::Clip(t, s) => return None, + }, + key_pat!(Char('q')) => match state.selected() { + Selected::Mix => return None, + Selected::Track(t) => return None, + Selected::Scene(s) => return None, + Selected::Clip(t, s) => return None, + }, _ => match state.selected() { - Mix => to_arranger_mix_command(input)?, - Track(t) => to_arranger_track_command(input, t)?, - Scene(s) => to_arranger_scene_command(input, s)?, - Clip(t, s) => to_arranger_clip_command(input, t, s)?, + Selected::Mix => to_arranger_mix_command(input)?, + Selected::Track(t) => to_arranger_track_command(input, t)?, + Selected::Scene(s) => to_arranger_scene_command(input, s)?, + Selected::Clip(t, s) => to_arranger_clip_command(input, t, s)?, } - }) -} + } +}); fn to_arranger_mix_command (input: &TuiInput) -> Option { - use KeyCode::{Char, Down, Right, Delete}; use ArrangerCommand as Cmd; use ArrangerSelection as Select; Some(match input.event() { + // 0: Enqueue phrase 0 (stop all) + key_pat!(Char('0')) => Cmd::StopAll, key_pat!(Char('s')) => Cmd::Select(Select::Scene(0)), key_pat!(Char('d')) => Cmd::Select(Select::Track(0)), key_pat!(Char(',')) => Cmd::Zoom(0), diff --git a/crates/tek/src/tui/arranger_scene.rs b/crates/tek/src/tui/arranger_scene.rs index 4f0139be..db61f32a 100644 --- a/crates/tek/src/tui/arranger_scene.rs +++ b/crates/tek/src/tui/arranger_scene.rs @@ -1,4 +1,99 @@ use crate::*; + +pub trait HasScenes { + fn scenes (&self) -> &Vec; + fn scenes_mut (&mut self) -> &mut Vec; + fn scene_add (&mut self, name: Option<&str>, color: Option) -> Usually<&mut S>; + fn scene_del (&mut self, index: usize) { + self.scenes_mut().remove(index); + } + fn scene_default_name (&self) -> String { + format!("Scene {}", self.scenes().len() + 1) + } + fn selected_scene (&self) -> Option<&S> { + None + } + fn selected_scene_mut (&mut self) -> Option<&mut S> { + None + } +} + +#[derive(Clone, Debug)] +pub enum ArrangerSceneCommand { + Add, + Delete(usize), + RandomColor, + Play(usize), + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), +} + +//impl Command for ArrangerSceneCommand { + //fn execute (self, state: &mut T) -> Perhaps { + //match self { + //Self::Delete(index) => { state.scene_del(index); }, + //_ => todo!() + //} + //Ok(None) + //} +//} + +pub trait ArrangerSceneApi: Sized { + fn name (&self) -> &Arc>; + fn clips (&self) -> &Vec>>>; + fn color (&self) -> ItemPalette; + + fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> { + let mut total = 0; + if factor == 0 { + scenes.iter().map(|scene|{ + let pulses = scene.pulses().max(PPQ); + total = total + pulses; + (pulses, total - pulses) + }).collect() + } else { + (0..=scenes.len()).map(|i|{ + (factor*PPQ, factor*PPQ*i) + }).collect() + } + } + + fn longest_name (scenes: &[Self]) -> usize { + scenes.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) + } + + /// Returns the pulse length of the longest phrase in the scene + 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 phrases in the scene are + /// currently playing on the given collection of tracks. + fn is_playing (&self, tracks: &[T]) -> bool { + self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate() + .all(|(track_index, clip)|match clip { + Some(clip) => tracks + .get(track_index) + .map(|track|{ + if let Some((_, Some(phrase))) = track.player().play_phrase() { + *phrase.read().unwrap() == *clip.read().unwrap() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) + } + + fn clip (&self, index: usize) -> Option<&Arc>> { + match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None } + } + +} pub fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { use KeyCode::{Char, Up, Down, Right, Enter, Delete}; use ArrangerCommand as Cmd; @@ -18,6 +113,7 @@ pub fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option return None }) } + impl HasScenes for ArrangerTui { fn scenes (&self) -> &Vec { &self.scenes diff --git a/crates/tek/src/tui/arranger_track.rs b/crates/tek/src/tui/arranger_track.rs index 74077109..cdb63b23 100644 --- a/crates/tek/src/tui/arranger_track.rs +++ b/crates/tek/src/tui/arranger_track.rs @@ -1,18 +1,104 @@ use crate::*; +use KeyCode::{Char, Down, Left, Right, Delete}; + +pub trait HasTracks: Send + Sync { + fn tracks (&self) -> &Vec; + fn tracks_mut (&mut self) -> &mut Vec; +} + +impl HasTracks for Vec { + fn tracks (&self) -> &Vec { + self + } + fn tracks_mut (&mut self) -> &mut Vec { + self + } +} + +pub trait ArrangerTracksApi: HasTracks { + fn track_add (&mut self, name: Option<&str>, color: Option)-> Usually<&mut T>; + fn track_del (&mut self, index: usize); + fn track_default_name (&self) -> String { + format!("Track {}", self.tracks().len() + 1) + } +} + +#[derive(Clone, Debug)] +pub enum ArrangerTrackCommand { + Add, + Delete(usize), + RandomColor, + Stop, + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), +} + +pub trait ArrangerTrackApi: HasPlayer + Send + Sync + Sized { + /// Name of track + fn name (&self) -> &Arc>; + /// Preferred width of track column + fn width (&self) -> usize; + /// Preferred width of track column + fn width_mut (&mut self) -> &mut usize; + /// Identifying color of track + fn color (&self) -> ItemPalette; + + fn longest_name (tracks: &[Self]) -> usize { + tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) + } + + const MIN_WIDTH: usize = 3; + + fn width_inc (&mut self) { + *self.width_mut() += 1; + } + + fn width_dec (&mut self) { + if self.width() > Self::MIN_WIDTH { + *self.width_mut() -= 1; + } + } +} + +/// Hosts the JACK callback for a collection of tracks +pub struct TracksAudio<'a, T: ArrangerTrackApi, H: HasTracks>( + // Track collection + pub &'a mut H, + /// Note buffer + pub &'a mut Vec, + /// Note chunk buffer + pub &'a mut Vec>>, + /// Marker + pub PhantomData, +); + +impl<'a, T: ArrangerTrackApi, H: HasTracks> Audio for TracksAudio<'a, T, H> { + #[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.tracks_mut().iter_mut() { + if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit { + return Control::Quit + } + } + Control::Continue + } +} pub fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option { - use KeyCode::{Char, Down, Left, Right, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerTrackCommand as Track; + use ArrangerCommand::*; + use ArrangerSelection as Selected; + use ArrangerTrackCommand as Tracks; Some(match input.event() { - key_pat!(Char('s')) => Cmd::Select(Select::Clip(t, 0)), - key_pat!(Char('a')) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }), - key_pat!(Char('d')) => Cmd::Select(Select::Track(t + 1)), - key_pat!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)), - key_pat!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)), - key_pat!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)), - key_pat!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)), - key_pat!(Delete) => Cmd::Track(Track::Delete(t)), + key_pat!(Char('s')) => Select(Selected::Clip(t, 0)), + key_pat!(Char('a')) => Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix }), + key_pat!(Char('d')) => Select(Selected::Track(t + 1)), + key_pat!(Char(',')) => Track(Tracks::Swap(t, t - 1)), + key_pat!(Char('.')) => Track(Tracks::Swap(t, t + 1)), + key_pat!(Char('<')) => Track(Tracks::Swap(t, t - 1)), + key_pat!(Char('>')) => Track(Tracks::Swap(t, t + 1)), + key_pat!(Delete) => Track(Tracks::Delete(t)), //key_pat!(Char('c')) => Cmd::Track(Track::Color(t, ItemPalette::random())), _ => return None }) From 9ba0f3401eba297af42db7397b957ac0018c2c61 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 18:42:50 +0100 Subject: [PATCH 030/905] remove todos --- crates/tek/src/plugin/lv2_gui.rs | 0 crates/tek/src/plugin/lv2_tui.rs | 0 crates/tek/src/plugin/vst2_tui.rs | 0 crates/tek/src/plugin/vst3_tui.rs | 0 crates/tek/src/tui/_todo_tui_plugin_lv2.rs | 46 --------------- .../tek/src/tui/_todo_tui_plugin_lv2_gui.rs | 58 ------------------- crates/tek/src/tui/_todo_tui_plugin_vst2.rs | 13 ----- crates/tek/src/tui/_todo_tui_plugin_vst3.rs | 1 - .../tui/{_todo_tui_mixer.rs => app_mixer.rs} | 0 .../{_todo_tui_plugin.rs => app_plugin.rs} | 0 10 files changed, 118 deletions(-) create mode 100644 crates/tek/src/plugin/lv2_gui.rs create mode 100644 crates/tek/src/plugin/lv2_tui.rs create mode 100644 crates/tek/src/plugin/vst2_tui.rs create mode 100644 crates/tek/src/plugin/vst3_tui.rs delete mode 100644 crates/tek/src/tui/_todo_tui_plugin_lv2.rs delete mode 100644 crates/tek/src/tui/_todo_tui_plugin_lv2_gui.rs delete mode 100644 crates/tek/src/tui/_todo_tui_plugin_vst2.rs delete mode 100644 crates/tek/src/tui/_todo_tui_plugin_vst3.rs rename crates/tek/src/tui/{_todo_tui_mixer.rs => app_mixer.rs} (100%) rename crates/tek/src/tui/{_todo_tui_plugin.rs => app_plugin.rs} (100%) diff --git a/crates/tek/src/plugin/lv2_gui.rs b/crates/tek/src/plugin/lv2_gui.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/tek/src/plugin/lv2_tui.rs b/crates/tek/src/plugin/lv2_tui.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/tek/src/plugin/vst2_tui.rs b/crates/tek/src/plugin/vst2_tui.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/tek/src/plugin/vst3_tui.rs b/crates/tek/src/plugin/vst3_tui.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/tek/src/tui/_todo_tui_plugin_lv2.rs b/crates/tek/src/tui/_todo_tui_plugin_lv2.rs deleted file mode 100644 index a71fea12..00000000 --- a/crates/tek/src/tui/_todo_tui_plugin_lv2.rs +++ /dev/null @@ -1,46 +0,0 @@ -use super::*; -use ::livi::{ - World, - Instance, - Plugin as LiviPlugin, - Features, - FeaturesBuilder, - Port, - event::LV2AtomSequence, -}; -use std::thread::JoinHandle; - -/// A LV2 plugin. -pub struct LV2Plugin { - pub world: World, - pub instance: Instance, - pub plugin: LiviPlugin, - pub features: Arc, - pub port_list: Vec, - pub input_buffer: Vec, - pub ui_thread: Option>, -} - -impl LV2Plugin { - const INPUT_BUFFER: usize = 1024; - pub fn new (uri: &str) -> Usually { - // Get 1st plugin at URI - let world = World::with_load_bundle(&uri); - let features = FeaturesBuilder { min_block_length: 1, max_block_length: 65536 }; - let features = world.build_features(features); - let mut plugin = None; - if let Some(p) = world.iter_plugins().next() { plugin = Some(p); } - let plugin = plugin.expect("plugin not found"); - let err = &format!("init {uri}"); - let instance = unsafe { plugin.instantiate(features.clone(), 48000.0).expect(&err) }; - let mut port_list = vec![]; - for port in plugin.ports() { - port_list.push(port); - } - let input_buffer = Vec::with_capacity(Self::INPUT_BUFFER); - // Instantiate - Ok(Self { - world, instance, port_list, plugin, features, input_buffer, ui_thread: None - }) - } -} diff --git a/crates/tek/src/tui/_todo_tui_plugin_lv2_gui.rs b/crates/tek/src/tui/_todo_tui_plugin_lv2_gui.rs deleted file mode 100644 index a296eee5..00000000 --- a/crates/tek/src/tui/_todo_tui_plugin_lv2_gui.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::*; -use std::thread::{spawn, JoinHandle}; -use ::winit::{ - application::ApplicationHandler, - event::WindowEvent, - event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, - window::{Window, WindowId}, - platform::x11::EventLoopBuilderExtX11 -}; - -//pub struct LV2PluginUI { - //write: (), - //controller: (), - //widget: (), - //features: (), - //transfer: (), -//} - -pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually> { - Ok(spawn(move||{ - let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap(); - event_loop.set_control_flow(ControlFlow::Wait); - event_loop.run_app(&mut ui).unwrap() - })) -} - -/// A LV2 plugin's X11 UI. -pub struct LV2PluginUI { - pub window: Option -} - -impl LV2PluginUI { - pub fn new () -> Usually { - Ok(Self { window: None }) - } -} - -impl ApplicationHandler for LV2PluginUI { - fn resumed (&mut self, event_loop: &ActiveEventLoop) { - self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); - } - fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { - match event { - WindowEvent::CloseRequested => { - self.window.as_ref().unwrap().set_visible(false); - event_loop.exit(); - }, - WindowEvent::RedrawRequested => { - self.window.as_ref().unwrap().request_redraw(); - } - _ => (), - } - } -} - -fn lv2_ui_instantiate (kind: &str) { - //let host = Suil -} diff --git a/crates/tek/src/tui/_todo_tui_plugin_vst2.rs b/crates/tek/src/tui/_todo_tui_plugin_vst2.rs deleted file mode 100644 index cb04e7e0..00000000 --- a/crates/tek/src/tui/_todo_tui_plugin_vst2.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::*; - -impl ::vst::host::Host for Plugin {} - -fn set_vst_plugin (host: &Arc>>, _path: &str) -> Usually { - let mut loader = ::vst::host::PluginLoader::load( - &std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"), - host.clone() - )?; - Ok(PluginKind::VST2 { - instance: loader.instance()? - }) -} diff --git a/crates/tek/src/tui/_todo_tui_plugin_vst3.rs b/crates/tek/src/tui/_todo_tui_plugin_vst3.rs deleted file mode 100644 index 46330df3..00000000 --- a/crates/tek/src/tui/_todo_tui_plugin_vst3.rs +++ /dev/null @@ -1 +0,0 @@ -//! TODO diff --git a/crates/tek/src/tui/_todo_tui_mixer.rs b/crates/tek/src/tui/app_mixer.rs similarity index 100% rename from crates/tek/src/tui/_todo_tui_mixer.rs rename to crates/tek/src/tui/app_mixer.rs diff --git a/crates/tek/src/tui/_todo_tui_plugin.rs b/crates/tek/src/tui/app_plugin.rs similarity index 100% rename from crates/tek/src/tui/_todo_tui_plugin.rs rename to crates/tek/src/tui/app_plugin.rs From e2492a1326fcee18c4508a698627944cd9abfc99 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 18:48:59 +0100 Subject: [PATCH 031/905] pass thru arranger commands to embedded sequencer --- crates/tek/src/tui/app_arranger.rs | 32 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index bd05a5f9..9c617157 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -134,21 +134,19 @@ input_to_command!(ArrangerCommand: |state:ArrangerTui,input|{ use ArrangerCommand::*; // WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor match input.event() { + // TODO: u: undo + key_pat!(Char('u')) => { todo!("undo") }, + // TODO: Shift-U: redo + key_pat!(Char('U')) => { todo!("redo") }, + // TODO: k: toggle on-screen keyboard + key_pat!(Ctrl-Char('k')) => { todo!("keyboard") }, + // Transport: Play/pause key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), // Transport: Play from start or rewind to start key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), - // TODO: u: undo - key_pat!(Char('u')) => - { todo!("undo") }, - // TODO: Shift-U: redo - key_pat!(Char('U')) => - { todo!("redo") }, - // TODO: k: toggle on-screen keyboard - key_pat!(Ctrl-Char('k')) => - { todo!("keyboard") }, key_pat!(Char('e')) => Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))), key_pat!(Char('l')) => @@ -170,11 +168,17 @@ input_to_command!(ArrangerCommand: |state:ArrangerTui,input|{ Selected::Clip(t, s) => return None, }, _ => match state.selected() { - Selected::Mix => to_arranger_mix_command(input)?, - Selected::Track(t) => to_arranger_track_command(input, t)?, - Selected::Scene(s) => to_arranger_scene_command(input, s)?, - Selected::Clip(t, s) => to_arranger_clip_command(input, t, s)?, - } + Selected::Mix => to_arranger_mix_command(input), + Selected::Track(t) => to_arranger_track_command(input, t), + Selected::Scene(s) => to_arranger_scene_command(input, s), + Selected::Clip(t, s) => to_arranger_clip_command(input, t, s), + }.or_else(||if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { + Some(Editor(command)) + } else if let Some(command) = PhrasesCommand::input_to_command(&state.phrases, input) { + Some(Phrases(command)) + } else { + None + })? } }); fn to_arranger_mix_command (input: &TuiInput) -> Option { From 326507f4000db0aef8c37250848b7049a8642f8b Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 19:22:51 +0100 Subject: [PATCH 032/905] remove unused fields from arranger --- crates/tek/src/cli/cli_arranger.rs | 24 +++++++---- crates/tek/src/core/color.rs | 14 +++---- crates/tek/src/plugin/lv2_gui.rs | 59 +++++++++++++++++++++++++++ crates/tek/src/plugin/lv2_tui.rs | 47 +++++++++++++++++++++ crates/tek/src/plugin/vst2_tui.rs | 14 +++++++ crates/tek/src/plugin/vst3_tui.rs | 2 + crates/tek/src/tui/app_arranger.rs | 55 ++++++++++++------------- crates/tek/src/tui/arranger_mode_v.rs | 3 +- 8 files changed, 172 insertions(+), 46 deletions(-) diff --git a/crates/tek/src/cli/cli_arranger.rs b/crates/tek/src/cli/cli_arranger.rs index dde92c12..dc5f589b 100644 --- a/crates/tek/src/cli/cli_arranger.rs +++ b/crates/tek/src/cli/cli_arranger.rs @@ -9,24 +9,32 @@ pub fn main () -> Usually<()> { #[command(version, about, long_about = None)] pub struct ArrangerCli { /// Name of JACK client - #[arg(short, long)] name: Option, + #[arg(short, long)] + name: Option, + /// Whether to include a transport toolbar (default: true) - #[arg(short, long, default_value_t = true)] transport: bool, + #[arg(short, long, default_value_t = true)] + transport: bool, + /// Number of tracks - #[arg(short = 'x', long, default_value_t = 8)] tracks: usize, + #[arg(short = 'x', long, default_value_t = 8)] + tracks: usize, + /// Number of scenes - #[arg(short, long, default_value_t = 8)] scenes: usize, + #[arg(short, long, default_value_t = 8)] + scenes: usize, } impl ArrangerCli { /// Run the arranger TUI from CLI arguments. fn run (&self) -> Usually<()> { - Tui::run(JackClient::new("tek_arranger")?.activate_with(|jack|{ + let mut client_name = String::from("tek_arranger"); + if let Some(name) = self.name.as_ref() { + client_name = name.clone(); + } + Tui::run(JackClient::new(client_name.as_str())?.activate_with(|jack|{ let mut app = ArrangerTui::try_from(jack)?; app.color = ItemPalette::random(); - if let Some(name) = self.name.as_ref() { - *app.name.write().unwrap() = name.clone(); - } let track_color_1 = ItemColor::random(); let track_color_2 = ItemColor::random(); for i in 0..self.tracks { diff --git a/crates/tek/src/core/color.rs b/crates/tek/src/core/color.rs index 48d10fec..81041984 100644 --- a/crates/tek/src/core/color.rs +++ b/crates/tek/src/core/color.rs @@ -61,18 +61,18 @@ impl From for ItemPalette { impl From for ItemPalette { fn from (base: ItemColor) -> Self { let mut light = base.okhsl.clone(); - light.lightness = (light.lightness * 4. / 3.).min(Okhsl::::max_lightness()); + light.lightness = (light.lightness * 1.3).min(Okhsl::::max_lightness()); let mut lighter = light.clone(); - lighter.lightness = (lighter.lightness * 5. / 3.).min(Okhsl::::max_lightness()); + lighter.lightness = (lighter.lightness * 1.3).min(Okhsl::::max_lightness()); let mut lightest = lighter.clone(); - lightest.lightness = (lightest.lightness * 4. / 3.).min(Okhsl::::max_lightness()); + lightest.lightness = (lightest.lightness * 1.3).min(Okhsl::::max_lightness()); let mut dark = base.okhsl.clone(); - dark.lightness = (dark.lightness * 0.75).max(Okhsl::::min_lightness()); - dark.saturation = (dark.saturation * 0.75).max(Okhsl::::min_saturation()); + dark.lightness = (dark.lightness * 0.75).max(Okhsl::::min_lightness()); + dark.saturation = (dark.saturation * 0.75).max(Okhsl::::min_saturation()); let mut darker = dark.clone(); - darker.lightness = (darker.lightness * 0.66).max(Okhsl::::min_lightness()); - darker.saturation = (darker.saturation * 0.66).max(Okhsl::::min_saturation()); + darker.lightness = (darker.lightness * 0.66).max(Okhsl::::min_lightness()); + darker.saturation = (darker.saturation * 0.66).max(Okhsl::::min_saturation()); let mut darkest = darker.clone(); darkest.lightness = (darkest.lightness * 0.50).max(Okhsl::::min_lightness()); darkest.saturation = (darkest.saturation * 0.50).max(Okhsl::::min_saturation()); diff --git a/crates/tek/src/plugin/lv2_gui.rs b/crates/tek/src/plugin/lv2_gui.rs index e69de29b..8b94a94c 100644 --- a/crates/tek/src/plugin/lv2_gui.rs +++ b/crates/tek/src/plugin/lv2_gui.rs @@ -0,0 +1,59 @@ +use crate::*; +use std::thread::{spawn, JoinHandle}; +use ::winit::{ + application::ApplicationHandler, + event::WindowEvent, + event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, + window::{Window, WindowId}, + platform::x11::EventLoopBuilderExtX11 +}; + +//pub struct LV2PluginUI { + //write: (), + //controller: (), + //widget: (), + //features: (), + //transfer: (), +//} + +pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually> { + Ok(spawn(move||{ + let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap(); + event_loop.set_control_flow(ControlFlow::Wait); + event_loop.run_app(&mut ui).unwrap() + })) +} + +/// A LV2 plugin's X11 UI. +pub struct LV2PluginUI { + pub window: Option +} + +impl LV2PluginUI { + pub fn new () -> Usually { + Ok(Self { window: None }) + } +} + +impl ApplicationHandler for LV2PluginUI { + fn resumed (&mut self, event_loop: &ActiveEventLoop) { + self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); + } + fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { + match event { + WindowEvent::CloseRequested => { + self.window.as_ref().unwrap().set_visible(false); + event_loop.exit(); + }, + WindowEvent::RedrawRequested => { + self.window.as_ref().unwrap().request_redraw(); + } + _ => (), + } + } +} + +fn lv2_ui_instantiate (kind: &str) { + //let host = Suil +} + diff --git a/crates/tek/src/plugin/lv2_tui.rs b/crates/tek/src/plugin/lv2_tui.rs index e69de29b..4ec242b0 100644 --- a/crates/tek/src/plugin/lv2_tui.rs +++ b/crates/tek/src/plugin/lv2_tui.rs @@ -0,0 +1,47 @@ + +use super::*; +use ::livi::{ + World, + Instance, + Plugin as LiviPlugin, + Features, + FeaturesBuilder, + Port, + event::LV2AtomSequence, +}; +use std::thread::JoinHandle; + +/// A LV2 plugin. +pub struct LV2Plugin { + pub world: World, + pub instance: Instance, + pub plugin: LiviPlugin, + pub features: Arc, + pub port_list: Vec, + pub input_buffer: Vec, + pub ui_thread: Option>, +} + +impl LV2Plugin { + const INPUT_BUFFER: usize = 1024; + pub fn new (uri: &str) -> Usually { + // Get 1st plugin at URI + let world = World::with_load_bundle(&uri); + let features = FeaturesBuilder { min_block_length: 1, max_block_length: 65536 }; + let features = world.build_features(features); + let mut plugin = None; + if let Some(p) = world.iter_plugins().next() { plugin = Some(p); } + let plugin = plugin.expect("plugin not found"); + let err = &format!("init {uri}"); + let instance = unsafe { plugin.instantiate(features.clone(), 48000.0).expect(&err) }; + let mut port_list = vec![]; + for port in plugin.ports() { + port_list.push(port); + } + let input_buffer = Vec::with_capacity(Self::INPUT_BUFFER); + // Instantiate + Ok(Self { + world, instance, port_list, plugin, features, input_buffer, ui_thread: None + }) + } +} diff --git a/crates/tek/src/plugin/vst2_tui.rs b/crates/tek/src/plugin/vst2_tui.rs index e69de29b..6f50cf96 100644 --- a/crates/tek/src/plugin/vst2_tui.rs +++ b/crates/tek/src/plugin/vst2_tui.rs @@ -0,0 +1,14 @@ +use crate::*; + +impl ::vst::host::Host for Plugin {} + +fn set_vst_plugin (host: &Arc>>, _path: &str) -> Usually { + let mut loader = ::vst::host::PluginLoader::load( + &std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"), + host.clone() + )?; + Ok(PluginKind::VST2 { + instance: loader.instance()? + }) +} + diff --git a/crates/tek/src/plugin/vst3_tui.rs b/crates/tek/src/plugin/vst3_tui.rs index e69de29b..0f3ed08a 100644 --- a/crates/tek/src/plugin/vst3_tui.rs +++ b/crates/tek/src/plugin/vst3_tui.rs @@ -0,0 +1,2 @@ +//! TODO + diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 9c617157..b86e44c9 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -1,48 +1,45 @@ use crate::*; use ClockCommand::{Play, Pause}; -use KeyCode::{Char, Down, Right, Delete}; +use KeyCode::{Char, Delete}; /// Root view for standalone `tek_arranger` pub struct ArrangerTui { - pub jack: Arc>, + jack: Arc>, pub clock: ClockModel, pub phrases: PhraseListModel, pub tracks: Vec, pub scenes: Vec, - pub name: Arc>, pub splits: [u16;2], pub selected: ArrangerSelection, pub mode: ArrangerMode, pub color: ItemPalette, pub size: Measure, - pub cursor: (usize, usize), - pub menu_bar: Option>, - pub status_bar: Option, - pub history: Vec, pub note_buf: Vec, pub midi_buf: Vec>>, pub editor: PhraseEditorModel, pub perf: PerfModel, } -from_jack!(|jack| ArrangerTui Self { - jack: jack.clone(), - clock: ClockModel::from(jack), - phrases: PhraseListModel::default(), - editor: PhraseEditorModel::default(), - selected: ArrangerSelection::Clip(0, 0), - scenes: vec![], - tracks: vec![], - color: TuiTheme::bg().into(), - history: vec![], - mode: ArrangerMode::V(1), - name: Arc::new(RwLock::new(String::new())), - size: Measure::new(), - cursor: (0, 0), - splits: [16, 20], - menu_bar: None, - status_bar: None, - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), +from_jack!(|jack| ArrangerTui { + let clock = ClockModel::from(jack); + let phrase = Arc::new(RwLock::new(Phrase::new( + "New", true, 4 * clock.timebase.ppq.get() as usize, + None, Some(ItemColor::random().into()) + ))); + Self { + clock, + phrases: PhraseListModel::from(&phrase), + editor: PhraseEditorModel::from(&phrase), + selected: ArrangerSelection::Clip(0, 0), + scenes: vec![], + tracks: vec![], + color: TuiTheme::bg().into(), + mode: ArrangerMode::V(1), + size: Measure::new(), + splits: [16, 20], + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + jack: jack.clone(), + } }); render!(|self: ArrangerTui|{ let arranger = ||lay!(|add|{ @@ -67,11 +64,11 @@ render!(|self: ArrangerTui|{ ])), } }); - let with_pool = |x|Split::right(false, self.splits[1], PhraseListView(&self.phrases), x); + let with_pool = |x|Split::left(false, self.splits[1], PhraseListView(&self.phrases), x); let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); let transport = TransportView::from((self, None, true)); let with_transport = |x|col!([row!(![&play, &transport]), &x]); - with_transport(col!([Fixed::h(self.splits[0], arranger()), with_pool(&self.editor),])) + with_transport(with_pool(col!([Fixed::h(self.splits[0], arranger()), &self.editor]))) }); audio!(|self: ArrangerTui, client, scope|{ // Start profiling cycle diff --git a/crates/tek/src/tui/arranger_mode_v.rs b/crates/tek/src/tui/arranger_mode_v.rs index 227d982f..0b09fc5e 100644 --- a/crates/tek/src/tui/arranger_mode_v.rs +++ b/crates/tek/src/tui/arranger_mode_v.rs @@ -162,8 +162,7 @@ render!(|self: ArrangerVHeader<'a>|row!( // name and width of track let name = track.name().read().unwrap(); let max_w = w.saturating_sub(1).min(name.len()).max(2); - let name = format!("▎{}", &name[0..max_w]); - let name = Tui::bold(true, Tui::fg(track.color.lightest.rgb, name)); + let name = Tui::bold(true, Tui::fg(track.color.lightest.rgb, format!("▎{}", &name[0..max_w]))); // beats elapsed let elapsed = if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() { let length = phrase.read().unwrap().length; From 72dd3756db60d7bfd2bad11c5d201e0a6af762b6 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 19:26:21 +0100 Subject: [PATCH 033/905] auto redraw phrase on create editor --- crates/tek/src/tui/app_arranger.rs | 2 +- crates/tek/src/tui/phrase_editor.rs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index b86e44c9..578143d0 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -34,7 +34,7 @@ from_jack!(|jack| ArrangerTui { color: TuiTheme::bg().into(), mode: ArrangerMode::V(1), size: Measure::new(), - splits: [16, 20], + splits: [12, 20], midi_buf: vec![vec![];65536], note_buf: vec![], perf: PerfModel::default(), diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 44b50a3f..cc613232 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -210,7 +210,9 @@ impl PhraseEditorModel { impl From<&Arc>> for PhraseEditorModel { fn from (phrase: &Arc>) -> Self { - Self::from(Some(phrase.clone())) + let mut model = Self::from(Some(phrase.clone())); + model.redraw(); + model } } @@ -218,6 +220,7 @@ impl From>>> for PhraseEditorModel { fn from (phrase: Option>>) -> Self { let mut model = Self::default(); *model.phrase_mut() = phrase; + model.redraw(); model } } From 0a5959473019cf790c49c9c52cbc90ea52d21043 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 19:57:26 +0100 Subject: [PATCH 034/905] start with 4 tracks; remove ArrangerSceneApi --- crates/tek/src/cli/cli_arranger.rs | 2 +- crates/tek/src/tui/app_arranger.rs | 29 ++++-- crates/tek/src/tui/arranger_scene.rs | 123 +++++++++---------------- crates/tek/src/tui/arranger_track.rs | 2 +- crates/tek/src/tui/piano_horizontal.rs | 5 +- 5 files changed, 70 insertions(+), 91 deletions(-) diff --git a/crates/tek/src/cli/cli_arranger.rs b/crates/tek/src/cli/cli_arranger.rs index dc5f589b..98a5e026 100644 --- a/crates/tek/src/cli/cli_arranger.rs +++ b/crates/tek/src/cli/cli_arranger.rs @@ -17,7 +17,7 @@ pub struct ArrangerCli { transport: bool, /// Number of tracks - #[arg(short = 'x', long, default_value_t = 8)] + #[arg(short = 'x', long, default_value_t = 4)] tracks: usize, /// Number of scenes diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 578143d0..60e2160f 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -44,7 +44,7 @@ from_jack!(|jack| ArrangerTui { render!(|self: ArrangerTui|{ let arranger = ||lay!(|add|{ let color = self.color; - add(&Fill::wh(Lozenge(Style::default().bg(color.light.rgb).fg(color.darker.rgb))))?; + //add(&Fill::wh(Lozenge(Style::default().bg(color.light.rgb).fg(color.darker.rgb))))?; add(&self.size)?; match self.mode { ArrangerMode::H => todo!("horizontal arranger"), @@ -64,11 +64,20 @@ render!(|self: ArrangerTui|{ ])), } }); - let with_pool = |x|Split::left(false, self.splits[1], PhraseListView(&self.phrases), x); - let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); - let transport = TransportView::from((self, None, true)); + let with_pool = |x|Split::left(false, self.splits[1], PhraseListView(&self.phrases), x); + let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); + let transport = TransportView::from((self, None, true)); let with_transport = |x|col!([row!(![&play, &transport]), &x]); - with_transport(with_pool(col!([Fixed::h(self.splits[0], arranger()), &self.editor]))) + let color = self.color; + let with_frame = |x|lay!([ + Fill::wh(Tui::bg(color.darkest.rgb, " ")), + Fill::wh(Lozenge(Style::default().bg(color.light.rgb).fg(color.darker.rgb))), + x + ]); + with_transport(with_pool(with_frame(row!([ + Fixed::w(30, arranger()), + Fill::wh(&self.editor) + ])))) }); audio!(|self: ArrangerTui, client, scope|{ // Start profiling cycle @@ -217,7 +226,15 @@ command!(|self:ArrangerCommand,state:ArrangerTui|{ _ => { todo!() } } }); -command!(|self:ArrangerSceneCommand,_state:ArrangerTui|None); +command!(|self:ArrangerSceneCommand,state:ArrangerTui|match self { + //Self::Delete(index) => { state.scene_del(index); }, + Self::SetColor(index, color) => { + let old = state.scenes[index].color; + state.scenes[index].color = color; + Some(Self::SetColor(index, old)) + }, + _ => None +}); command!(|self:ArrangerTrackCommand,_state:ArrangerTui|None); command!(|self:ArrangerClipCommand, _state:ArrangerTui|None); #[derive(Clone, Debug)] diff --git a/crates/tek/src/tui/arranger_scene.rs b/crates/tek/src/tui/arranger_scene.rs index db61f32a..b9039f38 100644 --- a/crates/tek/src/tui/arranger_scene.rs +++ b/crates/tek/src/tui/arranger_scene.rs @@ -1,50 +1,23 @@ use crate::*; - -pub trait HasScenes { - fn scenes (&self) -> &Vec; - fn scenes_mut (&mut self) -> &mut Vec; - fn scene_add (&mut self, name: Option<&str>, color: Option) -> Usually<&mut S>; - fn scene_del (&mut self, index: usize) { - self.scenes_mut().remove(index); - } - fn scene_default_name (&self) -> String { - format!("Scene {}", self.scenes().len() + 1) - } - fn selected_scene (&self) -> Option<&S> { - None - } - fn selected_scene_mut (&mut self) -> Option<&mut S> { - None - } +#[derive(Default, Debug, Clone)] 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, } - -#[derive(Clone, Debug)] -pub enum ArrangerSceneCommand { - Add, - Delete(usize), - RandomColor, - Play(usize), - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), -} - -//impl Command for ArrangerSceneCommand { - //fn execute (self, state: &mut T) -> Perhaps { - //match self { - //Self::Delete(index) => { state.scene_del(index); }, - //_ => todo!() - //} - //Ok(None) - //} -//} - -pub trait ArrangerSceneApi: Sized { - fn name (&self) -> &Arc>; - fn clips (&self) -> &Vec>>>; - fn color (&self) -> ItemPalette; - - fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> { +impl ArrangerScene { + pub fn name (&self) -> &Arc> { + &self.name + } + pub fn clips (&self) -> &Vec>>> { + &self.clips + } + pub fn color (&self) -> ItemPalette { + self.color + } + pub fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> { let mut total = 0; if factor == 0 { scenes.iter().map(|scene|{ @@ -58,21 +31,18 @@ pub trait ArrangerSceneApi: Sized { }).collect() } } - - fn longest_name (scenes: &[Self]) -> usize { + pub fn longest_name (scenes: &[Self]) -> usize { scenes.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) } - /// Returns the pulse length of the longest phrase in the scene - fn pulses (&self) -> usize { + 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 phrases in the scene are /// currently playing on the given collection of tracks. - fn is_playing (&self, tracks: &[T]) -> bool { + pub fn is_playing (&self, tracks: &[T]) -> bool { self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate() .all(|(track_index, clip)|match clip { Some(clip) => tracks @@ -88,11 +58,20 @@ pub trait ArrangerSceneApi: Sized { None => true }) } - - fn clip (&self, index: usize) -> Option<&Arc>> { + pub fn clip (&self, index: usize) -> Option<&Arc>> { match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None } } - +} +#[derive(Clone, Debug)] +pub enum ArrangerSceneCommand { + Add, + Delete(usize), + RandomColor, + Play(usize), + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), + SetColor(usize, ItemPalette), } pub fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { use KeyCode::{Char, Up, Down, Right, Enter, Delete}; @@ -108,20 +87,20 @@ pub fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option Cmd::Scene(Scene::Swap(s, s - 1)), key_pat!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)), key_pat!(Char('q')) => Cmd::Scene(Scene::Play(s)), + key_pat!(Char('c')) => Cmd::Scene(Scene::SetColor(s, ItemPalette::random())), key_pat!(Delete) => Cmd::Scene(Scene::Delete(s)), //key_pat!(Char('c')) => Cmd::Track(Scene::Color(s, ItemPalette::random())), _ => return None }) } - -impl HasScenes for ArrangerTui { - fn scenes (&self) -> &Vec { +impl ArrangerTui { + pub fn scenes (&self) -> &Vec { &self.scenes } - fn scenes_mut (&mut self) -> &mut Vec { + pub fn scenes_mut (&mut self) -> &mut Vec { &mut self.scenes } - fn scene_add (&mut self, name: Option<&str>, color: Option) + pub fn scene_add (&mut self, name: Option<&str>, color: Option) -> Usually<&mut ArrangerScene> { let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string()); @@ -134,29 +113,13 @@ impl HasScenes for ArrangerTui { let index = self.scenes().len() - 1; Ok(&mut self.scenes_mut()[index]) } - fn selected_scene (&self) -> Option<&ArrangerScene> { + fn scene_default_name (&self) -> String { + format!("S{:3>0}", self.scenes().len() + 1) + } + pub fn selected_scene (&self) -> Option<&ArrangerScene> { self.selected.scene().map(|s|self.scenes().get(s)).flatten() } - fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { + pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten() } } -#[derive(Default, Debug, Clone)] 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 ArrangerSceneApi for ArrangerScene { - fn name (&self) -> &Arc> { - &self.name - } - fn clips (&self) -> &Vec>>> { - &self.clips - } - fn color (&self) -> ItemPalette { - self.color - } -} diff --git a/crates/tek/src/tui/arranger_track.rs b/crates/tek/src/tui/arranger_track.rs index cdb63b23..79c7fa58 100644 --- a/crates/tek/src/tui/arranger_track.rs +++ b/crates/tek/src/tui/arranger_track.rs @@ -19,7 +19,7 @@ pub trait ArrangerTracksApi: HasTracks { fn track_add (&mut self, name: Option<&str>, color: Option)-> Usually<&mut T>; fn track_del (&mut self, index: usize); fn track_default_name (&self) -> String { - format!("Track {}", self.tracks().len() + 1) + format!("T{}", self.tracks().len() + 1) } } diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index 2990ff98..e3ed1ec1 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -60,19 +60,18 @@ impl PianoHorizontal { } render!(|self: PianoHorizontal|{ - let color = self.color; let keys = move||PianoHorizontalKeys(&self); let timeline = move||PianoHorizontalTimeline(&self); let notes = move||PianoHorizontalNotes(&self); let cursor = move||PianoHorizontalCursor(&self); let keys_width = 5; - Tui::bg(color.darker.rgb, Fill::wh(Bsp::s( + Fill::wh(Bsp::s( Fixed::h(1, Bsp::e(Fixed::w(keys_width, ""), Fill::w(timeline()),)), Bsp::e( Fixed::w(keys_width, keys()), Fill::wh(lay!([&self.size, Fill::wh(lay!([Fill::wh(notes()), Fill::wh(cursor()),]))])), ), - ))) + )) }); pub struct PianoHorizontalTimeline<'a>(&'a PianoHorizontal); From d07ed000344a92305c237751901c30c7b26717fe Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 19 Dec 2024 13:40:41 +0100 Subject: [PATCH 035/905] fix arranger inverse border --- crates/tek/src/tui/app_arranger.rs | 68 ++++++++++++--------------- crates/tek/src/tui/arranger_mode_v.rs | 4 +- crates/tek/src/tui/arranger_scene.rs | 2 +- 3 files changed, 32 insertions(+), 42 deletions(-) diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 60e2160f..a33d37b9 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -26,58 +26,48 @@ from_jack!(|jack| ArrangerTui { ))); Self { clock, - phrases: PhraseListModel::from(&phrase), - editor: PhraseEditorModel::from(&phrase), - selected: ArrangerSelection::Clip(0, 0), - scenes: vec![], - tracks: vec![], - color: TuiTheme::bg().into(), - mode: ArrangerMode::V(1), - size: Measure::new(), - splits: [12, 20], - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - jack: jack.clone(), + phrases: PhraseListModel::from(&phrase), + editor: PhraseEditorModel::from(&phrase), + selected: ArrangerSelection::Clip(0, 0), + scenes: vec![], + tracks: vec![], + color: TuiTheme::bg().into(), + mode: ArrangerMode::V(1), + size: Measure::new(), + splits: [12, 20], + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + jack: jack.clone(), } }); render!(|self: ArrangerTui|{ let arranger = ||lay!(|add|{ let color = self.color; - //add(&Fill::wh(Lozenge(Style::default().bg(color.light.rgb).fg(color.darker.rgb))))?; - add(&self.size)?; + add(&Fill::wh(Tui::bg(color.darkest.rgb, "x")))?; + add(&Fill::wh(Lozenge(Style::default().fg(color.light.rgb).bg(color.darker.rgb))))?; match self.mode { ArrangerMode::H => todo!("horizontal arranger"), - ArrangerMode::V(factor) => add(&lay!([ - Align::se(Fill::wh(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(true), - format!("{}x{}", self.size.w(), self.size.h())) - ))), - Tui::bg(color.darkest.rgb, lay!(![ - ArrangerVColumnSeparator::from(self), - ArrangerVRowSeparator::from((self, factor)), - col!(![ - ArrangerVHeader::from(self), - ArrangerVContent::from((self, factor)), - ]), - ArrangerVCursor::from((self, factor)), - ])), + ArrangerMode::V(factor) => add(&lay!(![ + ArrangerVColumnSeparator::from(self), + ArrangerVRowSeparator::from((self, factor)), + col!(![ + ArrangerVHeader::from(self), + ArrangerVContent::from((self, factor)), + ]), + ArrangerVCursor::from((self, factor)), ])), } }); - let with_pool = |x|Split::left(false, self.splits[1], PhraseListView(&self.phrases), x); let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); let transport = TransportView::from((self, None, true)); let with_transport = |x|col!([row!(![&play, &transport]), &x]); - let color = self.color; - let with_frame = |x|lay!([ - Fill::wh(Tui::bg(color.darkest.rgb, " ")), - Fill::wh(Lozenge(Style::default().bg(color.light.rgb).fg(color.darker.rgb))), - x - ]); - with_transport(with_pool(with_frame(row!([ - Fixed::w(30, arranger()), - Fill::wh(&self.editor) - ])))) + let with_pool = |x|Split::left(false, self.splits[1], PhraseListView(&self.phrases), x); + with_transport(with_pool(col!([ + &self.size, + Fill::w(Fixed::h(20, arranger())), + Fill::w(Fixed::h(25, &self.editor)), + ]))) }); audio!(|self: ArrangerTui, client, scope|{ // Start profiling cycle diff --git a/crates/tek/src/tui/arranger_mode_v.rs b/crates/tek/src/tui/arranger_mode_v.rs index 0b09fc5e..1c03986e 100644 --- a/crates/tek/src/tui/arranger_mode_v.rs +++ b/crates/tek/src/tui/arranger_mode_v.rs @@ -198,8 +198,8 @@ render!(|self: ArrangerVHeader<'a>|row!( .unwrap_or("(none)".into())); Tui::push_x(self.scenes_w, Tui::bg(track.color().base.rgb, - Tui::min_xy(w as u16, self.header_h, - col!([name, timer])))) + Tui::min_xy(w as u16, self.header_h, + col!([name, timer])))) } )); diff --git a/crates/tek/src/tui/arranger_scene.rs b/crates/tek/src/tui/arranger_scene.rs index b9039f38..61278579 100644 --- a/crates/tek/src/tui/arranger_scene.rs +++ b/crates/tek/src/tui/arranger_scene.rs @@ -114,7 +114,7 @@ impl ArrangerTui { Ok(&mut self.scenes_mut()[index]) } fn scene_default_name (&self) -> String { - format!("S{:3>0}", self.scenes().len() + 1) + format!("S{:3>}", self.scenes().len() + 1) } pub fn selected_scene (&self) -> Option<&ArrangerScene> { self.selected.scene().map(|s|self.scenes().get(s)).flatten() From 6be71a44138ebb8c5cc63189f680aff9197c56bd Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 19 Dec 2024 14:46:45 +0100 Subject: [PATCH 036/905] wip: add note with (Shift-)Enter --- crates/tek/src/tui/app_arranger.rs | 82 ++++++++++++++++----------- crates/tek/src/tui/arranger_clip.rs | 2 +- crates/tek/src/tui/arranger_mode_v.rs | 12 ++-- crates/tek/src/tui/arranger_scene.rs | 4 +- crates/tek/src/tui/arranger_track.rs | 4 +- crates/tek/src/tui/phrase_editor.rs | 5 +- 6 files changed, 65 insertions(+), 44 deletions(-) diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index a33d37b9..5eccb290 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -1,22 +1,23 @@ use crate::*; use ClockCommand::{Play, Pause}; -use KeyCode::{Char, Delete}; +use KeyCode::{Char, Delete, Tab}; /// Root view for standalone `tek_arranger` pub struct ArrangerTui { - jack: Arc>, - pub clock: ClockModel, - pub phrases: PhraseListModel, - pub tracks: Vec, - pub scenes: Vec, - pub splits: [u16;2], - pub selected: ArrangerSelection, - pub mode: ArrangerMode, - pub color: ItemPalette, - pub size: Measure, - pub note_buf: Vec, - pub midi_buf: Vec>>, - pub editor: PhraseEditorModel, - pub perf: PerfModel, + jack: Arc>, + pub clock: ClockModel, + pub phrases: PhraseListModel, + pub tracks: Vec, + pub scenes: Vec, + pub splits: [u16;2], + pub selected: ArrangerSelection, + pub mode: ArrangerMode, + pub color: ItemPalette, + pub size: Measure, + pub note_buf: Vec, + pub midi_buf: Vec>>, + pub editor: PhraseEditorModel, + pub perf: PerfModel, + pub show_pool: bool, } from_jack!(|jack| ArrangerTui { let clock = ClockModel::from(jack); @@ -26,19 +27,20 @@ from_jack!(|jack| ArrangerTui { ))); Self { clock, - phrases: PhraseListModel::from(&phrase), - editor: PhraseEditorModel::from(&phrase), - selected: ArrangerSelection::Clip(0, 0), - scenes: vec![], - tracks: vec![], - color: TuiTheme::bg().into(), - mode: ArrangerMode::V(1), - size: Measure::new(), - splits: [12, 20], - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - jack: jack.clone(), + phrases: PhraseListModel::from(&phrase), + editor: PhraseEditorModel::from(&phrase), + selected: ArrangerSelection::Clip(0, 0), + scenes: vec![], + tracks: vec![], + color: TuiTheme::bg().into(), + mode: ArrangerMode::V(1), + size: Measure::new(), + splits: [12, 20], + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + show_pool: false, + jack: jack.clone(), } }); render!(|self: ArrangerTui|{ @@ -62,7 +64,11 @@ render!(|self: ArrangerTui|{ let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); let transport = TransportView::from((self, None, true)); let with_transport = |x|col!([row!(![&play, &transport]), &x]); - let with_pool = |x|Split::left(false, self.splits[1], PhraseListView(&self.phrases), x); + let with_pool = |x|Split::left(false, if self.show_pool { + self.splits[1] + } else { + 0 + }, PhraseListView(&self.phrases), x); with_transport(with_pool(col!([ &self.size, Fill::w(Fixed::h(20, arranger())), @@ -124,6 +130,7 @@ handle!(|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, Zoom(usize), Phrases(PhrasesCommand), Editor(PhraseCommand), + ShowPool(bool) } input_to_command!(ArrangerCommand: |state:ArrangerTui,input|{ use ArrangerSelection as Selected; @@ -163,11 +170,18 @@ input_to_command!(ArrangerCommand: |state:ArrangerTui,input|{ Selected::Scene(s) => return None, Selected::Clip(t, s) => return None, }, + // Tab: Toggle visibility of phrase pool column + key_pat!(Tab) => + ShowPool(!state.show_pool), _ => match state.selected() { Selected::Mix => to_arranger_mix_command(input), - Selected::Track(t) => to_arranger_track_command(input, t), - Selected::Scene(s) => to_arranger_scene_command(input, s), - Selected::Clip(t, s) => to_arranger_clip_command(input, t, s), + Selected::Track(t) => to_arranger_track_command(input, + t, state.tracks.len()), + Selected::Scene(s) => to_arranger_scene_command(input, + s, state.scenes.len()), + Selected::Clip(t, s) => to_arranger_clip_command(input, + t, state.tracks.len(), + s, state.scenes.len()), }.or_else(||if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { Some(Editor(command)) } else if let Some(command) = PhrasesCommand::input_to_command(&state.phrases, input) { @@ -213,6 +227,10 @@ command!(|self:ArrangerCommand,state:ArrangerTui|{ state.color = palette; Some(Color(old)) }, + ShowPool(show) => { + state.show_pool = show; + None + }, _ => { todo!() } } }); diff --git a/crates/tek/src/tui/arranger_clip.rs b/crates/tek/src/tui/arranger_clip.rs index 040db3aa..9f61d1f1 100644 --- a/crates/tek/src/tui/arranger_clip.rs +++ b/crates/tek/src/tui/arranger_clip.rs @@ -1,6 +1,6 @@ use crate::*; -pub fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option { +pub fn to_arranger_clip_command (input: &TuiInput, t: usize, len_t: usize, s: usize, len_s: usize) -> Option { use KeyCode::{Char, Up, Down, Left, Right, Delete}; use ArrangerCommand as Cmd; use ArrangerSelection as Select; diff --git a/crates/tek/src/tui/arranger_mode_v.rs b/crates/tek/src/tui/arranger_mode_v.rs index 1c03986e..dc95b787 100644 --- a/crates/tek/src/tui/arranger_mode_v.rs +++ b/crates/tek/src/tui/arranger_mode_v.rs @@ -81,12 +81,16 @@ render!(|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{ let focused = self.focused; let selected = self.selected; let get_track_area = |t: usize| [ - self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(), - self.cols[t].0 as u16, area.h(), + self.scenes_w + area.x() + self.cols[t].1 as u16, + area.y(), + self.cols[t].0 as u16, + area.h(), ]; let get_scene_area = |s: usize| [ - area.x(), self.header_h + area.y() + (self.rows[s].1 / PPQ) as u16, - area.w(), (self.rows[s].0 / PPQ) as u16 + area.x(), + self.header_h + area.y() + (self.rows[s].1 / PPQ) as u16, + area.w(), + (self.rows[s].0 / PPQ) as u16 ]; let get_clip_area = |t: usize, s: usize| [ self.scenes_w + area.x() + self.cols[t].1 as u16, diff --git a/crates/tek/src/tui/arranger_scene.rs b/crates/tek/src/tui/arranger_scene.rs index 61278579..b04ff695 100644 --- a/crates/tek/src/tui/arranger_scene.rs +++ b/crates/tek/src/tui/arranger_scene.rs @@ -73,14 +73,14 @@ pub enum ArrangerSceneCommand { SetZoom(usize), SetColor(usize, ItemPalette), } -pub fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { +pub fn to_arranger_scene_command (input: &TuiInput, s: usize, len: usize) -> Option { use KeyCode::{Char, Up, Down, Right, Enter, Delete}; use ArrangerCommand as Cmd; use ArrangerSelection as Select; use ArrangerSceneCommand as Scene; Some(match input.event() { key_pat!(Char('w')) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }), - key_pat!(Char('s')) => Cmd::Select(Select::Scene(s + 1)), + key_pat!(Char('s')) => Cmd::Select(Select::Scene((s + 1) % len)), key_pat!(Char('d')) => Cmd::Select(Select::Clip(0, s)), key_pat!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)), key_pat!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)), diff --git a/crates/tek/src/tui/arranger_track.rs b/crates/tek/src/tui/arranger_track.rs index 79c7fa58..e99950df 100644 --- a/crates/tek/src/tui/arranger_track.rs +++ b/crates/tek/src/tui/arranger_track.rs @@ -86,14 +86,14 @@ impl<'a, T: ArrangerTrackApi, H: HasTracks> Audio for TracksAudio<'a, T, H> { Control::Continue } } -pub fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option { +pub fn to_arranger_track_command (input: &TuiInput, t: usize, len: usize) -> Option { use ArrangerCommand::*; use ArrangerSelection as Selected; use ArrangerTrackCommand as Tracks; Some(match input.event() { key_pat!(Char('s')) => Select(Selected::Clip(t, 0)), key_pat!(Char('a')) => Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix }), - key_pat!(Char('d')) => Select(Selected::Track(t + 1)), + key_pat!(Char('d')) => Select(Selected::Track((t + 1) % len)), key_pat!(Char(',')) => Track(Tracks::Swap(t, t - 1)), key_pat!(Char('.')) => Track(Tracks::Swap(t, t + 1)), key_pat!(Char('<')) => Track(Tracks::Swap(t, t - 1)), diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index cc613232..d78745e7 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -1,5 +1,4 @@ use crate::*; -use Ordering::Relaxed; use KeyCode::{Char, Up, Down, Left, Right}; use PhraseCommand::*; @@ -64,8 +63,8 @@ impl InputToCommand for PhraseCommand { key_pat!(Char('_')) => SetTimeZoom(next_note_length(time_zoom())), key_pat!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom())), key_pat!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom())), - key_pat!(Char('a')) => AppendNote, - key_pat!(Char('s')) => PutNote, + key_pat!(Enter) => PutNote, + key_pat!(Ctrl-Enter) => AppendNote, key_pat!(Char(',')) => SetNoteLength(prev_note_length(note_len())), // TODO: no 3plet key_pat!(Char('.')) => SetNoteLength(next_note_length(note_len())), key_pat!(Char('<')) => SetNoteLength(prev_note_length(note_len())), // TODO: 3plet From d806014df219f25abebb1e8893ce75e6c7b9a32e Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 19 Dec 2024 17:04:34 +0100 Subject: [PATCH 037/905] trim arranger view names --- crates/tek/src/tui/app_arranger.rs | 10 ++--- crates/tek/src/tui/arranger_mode_v.rs | 61 +++++++++++++++------------ crates/tek/src/tui/arranger_track.rs | 2 +- 3 files changed, 39 insertions(+), 34 deletions(-) diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 5eccb290..90fd03ea 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -46,16 +46,16 @@ from_jack!(|jack| ArrangerTui { render!(|self: ArrangerTui|{ let arranger = ||lay!(|add|{ let color = self.color; - add(&Fill::wh(Tui::bg(color.darkest.rgb, "x")))?; + add(&Fill::wh(Tui::bg(color.darkest.rgb, ())))?; add(&Fill::wh(Lozenge(Style::default().fg(color.light.rgb).bg(color.darker.rgb))))?; match self.mode { ArrangerMode::H => todo!("horizontal arranger"), ArrangerMode::V(factor) => add(&lay!(![ - ArrangerVColumnSeparator::from(self), - ArrangerVRowSeparator::from((self, factor)), + ArrangerVColSep::from(self), + ArrangerVRowSep::from((self, factor)), col!(![ - ArrangerVHeader::from(self), - ArrangerVContent::from((self, factor)), + ArrangerVHead::from(self), + ArrangerVBody::from((self, factor)), ]), ArrangerVCursor::from((self, factor)), ])), diff --git a/crates/tek/src/tui/arranger_mode_v.rs b/crates/tek/src/tui/arranger_mode_v.rs index dc95b787..92cb17d1 100644 --- a/crates/tek/src/tui/arranger_mode_v.rs +++ b/crates/tek/src/tui/arranger_mode_v.rs @@ -1,12 +1,12 @@ use crate::*; -pub struct ArrangerVColumnSeparator { +pub struct ArrangerVColSep { cols: Vec<(usize, usize)>, scenes_w: u16, sep_fg: Color, } -impl From<&ArrangerTui> for ArrangerVColumnSeparator { +impl From<&ArrangerTui> for ArrangerVColSep { fn from (state: &ArrangerTui) -> Self { Self { cols: track_widths(state.tracks()), @@ -16,7 +16,7 @@ impl From<&ArrangerTui> for ArrangerVColumnSeparator { } } -render!(|self: ArrangerVColumnSeparator|render(move|to: &mut TuiOutput|{ +render!(|self: ArrangerVColSep|render(move|to: &mut TuiOutput|{ let style = Some(Style::default().fg(self.sep_fg)); Ok(for x in self.cols.iter().map(|col|col.1) { let x = self.scenes_w + to.area().x() + x as u16; @@ -26,12 +26,12 @@ render!(|self: ArrangerVColumnSeparator|render(move|to: &mut TuiOutput|{ }) })); -pub struct ArrangerVRowSeparator { +pub struct ArrangerVRowSep { rows: Vec<(usize, usize)>, sep_fg: Color, } -impl From<(&ArrangerTui, usize)> for ArrangerVRowSeparator { +impl From<(&ArrangerTui, usize)> for ArrangerVRowSep { fn from ((state, factor): (&ArrangerTui, usize)) -> Self { Self { rows: ArrangerScene::ppqs(state.scenes(), factor), @@ -40,7 +40,7 @@ impl From<(&ArrangerTui, usize)> for ArrangerVRowSeparator { } } -render!(|self: ArrangerVRowSeparator|render(move|to: &mut TuiOutput|{ +render!(|self: ArrangerVRowSep|render(move|to: &mut TuiOutput|{ Ok(for y in self.rows.iter().map(|row|row.1) { let y = to.area().y() + (y / PPQ) as u16 + 1; if y >= to.buffer.area.height { break } @@ -135,7 +135,7 @@ render!(|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{ }) })); -pub struct ArrangerVHeader<'a> { +pub struct ArrangerVHead<'a> { tracks: &'a Vec, cols: Vec<(usize, usize)>, focused: bool, @@ -146,7 +146,7 @@ pub struct ArrangerVHeader<'a> { current: &'a Arc, } -impl<'a> From<&'a ArrangerTui> for ArrangerVHeader<'a> { +impl<'a> From<&'a ArrangerTui> for ArrangerVHead<'a> { fn from (state: &'a ArrangerTui) -> Self { Self { tracks: &state.tracks, @@ -161,12 +161,20 @@ impl<'a> From<&'a ArrangerTui> for ArrangerVHeader<'a> { } } -render!(|self: ArrangerVHeader<'a>|row!( +render!(|self: ArrangerVHead<'a>|row!( (track, w) in self.tracks.iter().zip(self.cols.iter().map(|col|col.0)) => { + // name and width of track - let name = track.name().read().unwrap(); - let max_w = w.saturating_sub(1).min(name.len()).max(2); - let name = Tui::bold(true, Tui::fg(track.color.lightest.rgb, format!("▎{}", &name[0..max_w]))); + let name = track.name().read().unwrap(); + let max_w = w.saturating_sub(1).min(name.len()).max(2); + let name = Tui::bold(true, Tui::fg(track.color.lightest.rgb, format!("▎{}", &name[0..max_w]))); + + // name of active MIDI input + let input = format!(">{}", track.player.midi_ins().get(0) + .map(|port|port.short_name()) + .transpose()? + .unwrap_or("(none)".into())); + // beats elapsed let elapsed = if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() { let length = phrase.read().unwrap().length; @@ -174,40 +182,37 @@ render!(|self: ArrangerVHeader<'a>|row!( let elapsed = self.timebase.format_beats_1_short( (elapsed as usize % length) as f64 ); - format!("▎+{elapsed:>}") + format!("+{elapsed:>}") } else { - String::from("▎") + String::new() }; + // beats until switchover let until_next = track.player.next_phrase().as_ref().map(|(t, _)|{ let target = t.pulse.get(); let current = self.current.pulse.get(); if target > current { let remaining = target - current; - format!("▎-{:>}", self.timebase.format_beats_0_short(remaining)) + format!("-{:>}", self.timebase.format_beats_0_short(remaining)) } else { String::new() } - }).unwrap_or(String::from("▎")); - let timer = col!([until_next, elapsed]); - // name of active MIDI input - let _input = format!("▎>{}", track.player.midi_ins().get(0) - .map(|port|port.short_name()) - .transpose()? - .unwrap_or("(none)".into())); + }); + // name of active MIDI output - let _output = format!("▎<{}", track.player.midi_outs().get(0) + let output = format!("<{}", track.player.midi_outs().get(0) .map(|port|port.short_name()) .transpose()? .unwrap_or("(none)".into())); Tui::push_x(self.scenes_w, Tui::bg(track.color().base.rgb, - Tui::min_xy(w as u16, self.header_h, - col!([name, timer])))) + Tui::min_xy(w as u16, self.header_h, row!([ + col!(!["▎", "▎", "▎", "▎", "▎", "▎",]), + col!(![name, input, output, elapsed, until_next, output])])))) } )); -pub struct ArrangerVContent<'a> { +pub struct ArrangerVBody<'a> { size: &'a Measure, scenes: &'a Vec, tracks: &'a Vec, @@ -216,7 +221,7 @@ pub struct ArrangerVContent<'a> { header_h: u16, } -impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVContent<'a> { +impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVBody<'a> { fn from ((state, factor): (&'a ArrangerTui, usize)) -> Self { Self { size: &state.size, @@ -229,7 +234,7 @@ impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVContent<'a> { } } -render!(|self: ArrangerVContent<'a>|Fixed::h( +render!(|self: ArrangerVBody<'a>|Fixed::h( (self.size.h() as u16).saturating_sub(self.header_h), col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => { let height = 1.max((pulses / PPQ) as u16); diff --git a/crates/tek/src/tui/arranger_track.rs b/crates/tek/src/tui/arranger_track.rs index e99950df..41dee088 100644 --- a/crates/tek/src/tui/arranger_track.rs +++ b/crates/tek/src/tui/arranger_track.rs @@ -48,7 +48,7 @@ pub trait ArrangerTrackApi: HasPlayer + Send + Sync + Sized { tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) } - const MIN_WIDTH: usize = 3; + const MIN_WIDTH: usize = 6; fn width_inc (&mut self) { *self.width_mut() += 1; From 77ea2a9b0226c7d3e08c3d32b3107a7aadda2c8a Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 19 Dec 2024 17:20:35 +0100 Subject: [PATCH 038/905] add from! macro --- crates/tek/src/core.rs | 10 ++++++++++ crates/tek/src/tui/arranger_mode_v.rs | 28 +++++++++++++++------------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/crates/tek/src/core.rs b/crates/tek/src/core.rs index d90e39c8..b23e43b1 100644 --- a/crates/tek/src/core.rs +++ b/crates/tek/src/core.rs @@ -10,3 +10,13 @@ pub use self::{ input::Handle, output::Render }; + +/// Prototypal case of implementor macro. +/// Saves 4loc per data pats. +#[macro_export] macro_rules! from { + (|$self:ident: $Source:ty| $Target:ty = $cb:expr) => { + impl From<$Source> for $Target { + fn from (state: $Source) -> Self { $cb } + } + }; +} diff --git a/crates/tek/src/tui/arranger_mode_v.rs b/crates/tek/src/tui/arranger_mode_v.rs index 92cb17d1..0af95251 100644 --- a/crates/tek/src/tui/arranger_mode_v.rs +++ b/crates/tek/src/tui/arranger_mode_v.rs @@ -5,17 +5,11 @@ pub struct ArrangerVColSep { scenes_w: u16, sep_fg: Color, } - -impl From<&ArrangerTui> for ArrangerVColSep { - fn from (state: &ArrangerTui) -> Self { - Self { - cols: track_widths(state.tracks()), - scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, - sep_fg: TuiTheme::separator_fg(false), - } - } -} - +from!(|source:&ArrangerTui|ArrangerVColSep = Self { + cols: track_widths(state.tracks()), + scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, + sep_fg: TuiTheme::separator_fg(false), +}); render!(|self: ArrangerVColSep|render(move|to: &mut TuiOutput|{ let style = Some(Style::default().fg(self.sep_fg)); Ok(for x in self.cols.iter().map(|col|col.1) { @@ -145,8 +139,18 @@ pub struct ArrangerVHead<'a> { timebase: &'a Arc, current: &'a Arc, } +from!(<'a>|state: &'a ArrangerTui|ArrangerVHead<'a>, { // A + tracks: &state.tracks, + cols: track_widths(state.tracks()), + focused: true, + selected: state.selected, + scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, + header_h: 3, + timebase: state.clock().timebase(), + current: &state.clock().playhead, +}); -impl<'a> From<&'a ArrangerTui> for ArrangerVHead<'a> { +impl<'a> From<&'a ArrangerTui> for ArrangerVHead<'a> { // B fn from (state: &'a ArrangerTui) -> Self { Self { tracks: &state.tracks, From 7620739e0df2bd73ef251ef4eeba20431ba53fe0 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 19 Dec 2024 17:38:30 +0100 Subject: [PATCH 039/905] some from! trait invocations --- crates/tek/src/core.rs | 6 ++-- crates/tek/src/core/color.rs | 9 ++--- crates/tek/src/midi.rs | 49 +++++++++++---------------- crates/tek/src/midi/midi_note.rs | 26 +++++++------- crates/tek/src/plugin.rs | 8 +---- crates/tek/src/tui/arranger_mode_v.rs | 20 ++--------- crates/tek/src/tui/status_bar.rs | 28 +++++++-------- 7 files changed, 53 insertions(+), 93 deletions(-) diff --git a/crates/tek/src/core.rs b/crates/tek/src/core.rs index b23e43b1..621074be 100644 --- a/crates/tek/src/core.rs +++ b/crates/tek/src/core.rs @@ -14,9 +14,9 @@ pub use self::{ /// Prototypal case of implementor macro. /// Saves 4loc per data pats. #[macro_export] macro_rules! from { - (|$self:ident: $Source:ty| $Target:ty = $cb:expr) => { - impl From<$Source> for $Target { - fn from (state: $Source) -> Self { $cb } + ($(<$lt:lifetime>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => { + impl $(<$lt>)? From<$Source> for $Target { + fn from ($state:$Source) -> Self { $cb } } }; } diff --git a/crates/tek/src/core/color.rs b/crates/tek/src/core/color.rs index 81041984..0c96d043 100644 --- a/crates/tek/src/core/color.rs +++ b/crates/tek/src/core/color.rs @@ -25,13 +25,10 @@ pub struct ItemPalette { pub darkest: ItemColor, } /// Adds TUI RGB representation to an OKHSL value. -impl From> for ItemColor { - fn from (okhsl: Okhsl) -> Self { Self { okhsl, rgb: okhsl_to_rgb(okhsl) } } -} +from!(|okhsl: Okhsl|ItemColor = Self { okhsl, rgb: okhsl_to_rgb(okhsl) }); /// Adds OKHSL representation to a TUI RGB value. -impl From for ItemColor { - fn from (rgb: Color) -> Self { Self { rgb, okhsl: rgb_to_okhsl(rgb) } } -} +from!(|rgb: Color|ItemColor = Self { rgb, okhsl: rgb_to_okhsl(rgb) }); +// A single color within item theme parameters, in OKHSL and RGB representations. impl ItemColor { pub fn random () -> Self { let mut rng = thread_rng(); diff --git a/crates/tek/src/midi.rs b/crates/tek/src/midi.rs index 9e8ff123..22300e80 100644 --- a/crates/tek/src/midi.rs +++ b/crates/tek/src/midi.rs @@ -101,7 +101,6 @@ pub struct PhrasePlayerModel { /// MIDI output buffer pub note_buf: Vec, } - impl std::fmt::Debug for PhrasePlayerModel { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("PhrasePlayerModel") @@ -111,34 +110,26 @@ impl std::fmt::Debug for PhrasePlayerModel { .finish() } } - -impl From<&ClockModel> for PhrasePlayerModel { - fn from (clock: &ClockModel) -> Self { - Self { - clock: clock.clone(), - midi_ins: vec![], - midi_outs: vec![], - note_buf: vec![0;8], - reset: true, - recording: false, - monitoring: false, - overdub: false, - play_phrase: None, - next_phrase: None, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - } - } -} - -impl From<(&ClockModel, &Arc>)> for PhrasePlayerModel { - fn from ((clock, phrase): (&ClockModel, &Arc>)) -> Self { - let mut model = Self::from(clock); - model.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone()))); - model - } -} - +from!(|clock: &ClockModel| PhrasePlayerModel = Self { + clock: clock.clone(), + midi_ins: vec![], + midi_outs: vec![], + note_buf: vec![0;8], + reset: true, + recording: false, + monitoring: false, + overdub: false, + play_phrase: None, + next_phrase: None, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), +}); +from!(|state: (&ClockModel, &Arc>)|PhrasePlayerModel = { + let (clock, phrase) = state; + let mut model = Self::from(clock); + model.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone()))); + model +}); has_clock!(|self:PhrasePlayerModel|&self.clock); impl HasMidiIns for PhrasePlayerModel { diff --git a/crates/tek/src/midi/midi_note.rs b/crates/tek/src/midi/midi_note.rs index abbc4bc0..408bfddb 100644 --- a/crates/tek/src/midi/midi_note.rs +++ b/crates/tek/src/midi/midi_note.rs @@ -1,6 +1,5 @@ use crate::*; use Ordering::Relaxed; - pub trait MidiViewport: MidiRange + MidiPoint + HasSize { /// Make sure cursor is within range fn autoscroll (&self) { @@ -17,8 +16,11 @@ pub trait MidiViewport: MidiRange + MidiPoint + HasSize { /// Make sure best usage of screen space is achieved by default fn autozoom (&self) { } + fn autozoom_n (&self) { + } + fn autozoom_t (&self) { + } } - #[derive(Debug, Clone)] pub struct MidiRangeModel { /// Length of visible time axis @@ -34,18 +36,14 @@ pub struct MidiRangeModel { // Lowest note displayed pub note_lo: Arc, } -impl From<(usize, bool)> for MidiRangeModel { - fn from ((time_zoom, time_lock): (usize, bool)) -> Self { - Self { - note_axis: Arc::new(0.into()), - note_lo: Arc::new(0.into()), - time_axis: Arc::new(0.into()), - time_start: Arc::new(0.into()), - time_zoom: Arc::new(time_zoom.into()), - time_lock: Arc::new(time_lock.into()), - } - } -} +from!(|data:(usize, bool)|MidiRangeModel = Self { + note_axis: Arc::new(0.into()), + note_lo: Arc::new(0.into()), + time_axis: Arc::new(0.into()), + time_start: Arc::new(0.into()), + time_zoom: Arc::new(data.0.into()), + time_lock: Arc::new(data.1.into()), +}); pub trait MidiRange { fn time_zoom (&self) -> usize; fn set_time_zoom (&mut self, x: usize); diff --git a/crates/tek/src/plugin.rs b/crates/tek/src/plugin.rs index 086f17bf..efbe4cd0 100644 --- a/crates/tek/src/plugin.rs +++ b/crates/tek/src/plugin.rs @@ -76,13 +76,7 @@ impl Plugin { } pub struct PluginAudio(Arc>); - -impl From<&Arc>> for PluginAudio { - fn from (model: &Arc>) -> Self { - Self(model.clone()) - } -} - +from!(|model: &Arc>| PluginAudio = Self(model.clone())); audio!(|self: PluginAudio, client_, _scope|{ let state = &mut*self.0.write().unwrap(); match state.plugin.as_mut() { diff --git a/crates/tek/src/tui/arranger_mode_v.rs b/crates/tek/src/tui/arranger_mode_v.rs index 0af95251..1a6c4608 100644 --- a/crates/tek/src/tui/arranger_mode_v.rs +++ b/crates/tek/src/tui/arranger_mode_v.rs @@ -5,7 +5,7 @@ pub struct ArrangerVColSep { scenes_w: u16, sep_fg: Color, } -from!(|source:&ArrangerTui|ArrangerVColSep = Self { +from!(|state:&ArrangerTui|ArrangerVColSep = Self { cols: track_widths(state.tracks()), scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, sep_fg: TuiTheme::separator_fg(false), @@ -139,7 +139,7 @@ pub struct ArrangerVHead<'a> { timebase: &'a Arc, current: &'a Arc, } -from!(<'a>|state: &'a ArrangerTui|ArrangerVHead<'a>, { // A +from!(<'a>|state: &'a ArrangerTui|ArrangerVHead<'a> = Self { // A tracks: &state.tracks, cols: track_widths(state.tracks()), focused: true, @@ -149,22 +149,6 @@ from!(<'a>|state: &'a ArrangerTui|ArrangerVHead<'a>, { // A timebase: state.clock().timebase(), current: &state.clock().playhead, }); - -impl<'a> From<&'a ArrangerTui> for ArrangerVHead<'a> { // B - fn from (state: &'a ArrangerTui) -> Self { - Self { - tracks: &state.tracks, - cols: track_widths(state.tracks()), - focused: true, - selected: state.selected, - scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, - header_h: 3, - timebase: state.clock().timebase(), - current: &state.clock().playhead, - } - } -} - render!(|self: ArrangerVHead<'a>|row!( (track, w) in self.tracks.iter().zip(self.cols.iter().map(|col|col.0)) => { diff --git a/crates/tek/src/tui/status_bar.rs b/crates/tek/src/tui/status_bar.rs index 05f47c3e..6b67e352 100644 --- a/crates/tek/src/tui/status_bar.rs +++ b/crates/tek/src/tui/status_bar.rs @@ -41,23 +41,19 @@ impl Bar for SequencerStatus { todo!() } } - -impl From<&SequencerTui> for SequencerStatus { - fn from (state: &SequencerTui) -> Self { - let samples = state.clock.chunk.load(Ordering::Relaxed); - let rate = state.clock.timebase.sr.get() as f64; - let buffer = samples as f64 / rate; - let width = state.size.w(); - Self { - width, - playing: state.clock.is_rolling(), - cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), - size: format!("{}x{}│", width, state.size.h()), - res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), - } +from!(|state:&SequencerTui|SequencerStatus = { + let samples = state.clock.chunk.load(Ordering::Relaxed); + let rate = state.clock.timebase.sr.get() as f64; + let buffer = samples as f64 / rate; + let width = state.size.w(); + Self { + width, + playing: state.clock.is_rolling(), + cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), + size: format!("{}x{}│", width, state.size.h()), + res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), } -} - +}); render!(|self: SequencerStatus|Fixed::h(2, lay!([ { let single = |binding, command|row!([" ", col!([ From 69bc8e69fd5b28ca0a9284b0139aa78f0dbfdf86 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 19 Dec 2024 17:51:47 +0100 Subject: [PATCH 040/905] autofix ~200 warnings --- crates/tek/src/audio.rs | 4 ++-- crates/tek/src/jack.rs | 5 ++--- crates/tek/src/lib.rs | 4 ++-- crates/tek/src/space.rs | 10 +++++----- crates/tek/src/space/stack.rs | 4 ++-- crates/tek/src/time/unit.rs | 1 - crates/tek/src/tui.rs | 7 +++---- crates/tek/src/tui/app_groovebox.rs | 1 - crates/tek/src/tui/app_sequencer.rs | 2 +- crates/tek/src/tui/arranger_clip.rs | 2 +- crates/tek/src/tui/arranger_scene.rs | 2 +- crates/tek/src/tui/arranger_track.rs | 2 +- crates/tek/src/tui/file_browser.rs | 3 --- crates/tek/src/tui/phrase_list.rs | 2 +- 14 files changed, 21 insertions(+), 28 deletions(-) diff --git a/crates/tek/src/audio.rs b/crates/tek/src/audio.rs index 46aa0557..a44f6276 100644 --- a/crates/tek/src/audio.rs +++ b/crates/tek/src/audio.rs @@ -1,3 +1,3 @@ -pub(crate) mod audio_in; pub(crate) use audio_in::*; -pub(crate) mod audio_out; pub(crate) use audio_out::*; +pub(crate) mod audio_in; +pub(crate) mod audio_out; pub(crate) mod sampler; pub(crate) use sampler::*; diff --git a/crates/tek/src/jack.rs b/crates/tek/src/jack.rs index aabea352..44332681 100644 --- a/crates/tek/src/jack.rs +++ b/crates/tek/src/jack.rs @@ -1,9 +1,8 @@ -use crate::*; -pub(crate) mod activate; pub(crate) use self::activate::*; +pub(crate) mod activate; pub(crate) mod audio; pub(crate) use self::audio::*; pub(crate) mod client; pub(crate) use self::client::*; -pub(crate) mod from_jack; pub(crate) use self::from_jack::*; +pub(crate) mod from_jack; pub(crate) mod jack_event; pub(crate) use self::jack_event::*; pub(crate) mod ports; pub(crate) use self::ports::*; diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs index abe73129..a13ac2f9 100644 --- a/crates/tek/src/lib.rs +++ b/crates/tek/src/lib.rs @@ -2,7 +2,7 @@ pub mod core; pub(crate) use self::core::*; pub mod time; pub(crate) use self::time::*; pub mod space; pub(crate) use self::space::*; pub mod tui; pub(crate) use self::tui::*; -pub mod edn; pub(crate) use self::edn::*; +pub mod edn; pub mod jack; pub(crate) use self::jack::*; pub mod midi; pub(crate) use self::midi::*; pub mod audio; pub(crate) use self::audio::*; @@ -71,7 +71,7 @@ pub(crate) use ::jack::{ Client, AsyncClient, ClientOptions, ClientStatus, ProcessScope, Control, CycleTimes, Port, PortId, - PortSpec, MidiIn, MidiOut, AudioIn, AudioOut, Unowned, + PortSpec, MidiIn, MidiOut, AudioOut, Unowned, Transport, TransportState, MidiIter, RawMidi, Frames, NotificationHandler, diff --git a/crates/tek/src/space.rs b/crates/tek/src/space.rs index bdad4973..1934f727 100644 --- a/crates/tek/src/space.rs +++ b/crates/tek/src/space.rs @@ -8,17 +8,17 @@ pub(crate) mod direction; pub(crate) use direction::*; ////////////////////////////////////////////////////// -pub(crate) mod align; pub(crate) use align::*; -pub(crate) mod bsp; pub(crate) use bsp::*; -pub(crate) mod cond; pub(crate) use cond::*; -pub(crate) mod fill; pub(crate) use fill::*; +pub(crate) mod align; +pub(crate) mod bsp; +pub(crate) mod cond; +pub(crate) mod fill; pub(crate) mod fixed; pub(crate) use fixed::*; pub(crate) mod inset_outset; pub(crate) use inset_outset::*; pub(crate) mod layers; pub(crate) use layers::*; pub(crate) mod measure; pub(crate) use measure::*; pub(crate) mod min_max; pub(crate) use min_max::*; pub(crate) mod push_pull; pub(crate) use push_pull::*; -pub(crate) mod scroll; pub(crate) use scroll::*; +pub(crate) mod scroll; pub(crate) mod shrink_grow; pub(crate) use shrink_grow::*; pub(crate) mod split; pub(crate) use split::*; pub(crate) mod stack; pub(crate) use stack::*; diff --git a/crates/tek/src/space/stack.rs b/crates/tek/src/space/stack.rs index 24daf4be..914217d9 100644 --- a/crates/tek/src/space/stack.rs +++ b/crates/tek/src/space/stack.rs @@ -130,8 +130,8 @@ where }, Direction::Left => { - let mut w: E::Unit = 0.into(); - let mut h: E::Unit = 0.into(); + let w: E::Unit = 0.into(); + let h: E::Unit = 0.into(); (self.0)(&mut |component: &dyn Render| { if w < to.w() { todo!(); diff --git a/crates/tek/src/time/unit.rs b/crates/tek/src/time/unit.rs index a0ad30e7..940cd65b 100644 --- a/crates/tek/src/time/unit.rs +++ b/crates/tek/src/time/unit.rs @@ -1,4 +1,3 @@ -use crate::*; /// A unit of time, represented as an atomic 64-bit float. /// diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index fcf1ba66..787d40e2 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -11,7 +11,6 @@ pub use tui_output::TuiOutput; //////////////////////////////////////////////////////// mod tui_style; -pub(crate) use tui_style::*; mod tui_theme; pub(crate) use tui_theme::*; mod tui_border; @@ -22,7 +21,7 @@ pub(crate) use tui_border::*; mod app_transport; pub(crate) use app_transport::*; mod app_sequencer; pub(crate) use app_sequencer::*; mod app_sampler; pub(crate) use app_sampler::*; -mod app_groovebox; pub(crate) use app_groovebox::*; +mod app_groovebox; mod app_arranger; pub(crate) use app_arranger::*; /////////////////////////////////////////////////////// @@ -31,7 +30,7 @@ mod arranger_clip; pub(crate) use arranger_clip::*; mod arranger_scene; pub(crate) use arranger_scene::*; mod arranger_select; pub(crate) use arranger_select::*; mod arranger_track; pub(crate) use arranger_track::*; -mod arranger_mode_h; pub(crate) use arranger_mode_h::*; +mod arranger_mode_h; mod arranger_mode_v; pub(crate) use arranger_mode_v::*; //////////////////////////////////////////////////////// @@ -43,7 +42,7 @@ mod piano_horizontal; pub(crate) use piano_horizontal::*; mod phrase_length; pub(crate) use phrase_length::*; mod phrase_rename; pub(crate) use phrase_rename::*; mod phrase_list; pub(crate) use phrase_list::*; -mod port_select; pub(crate) use port_select::*; +mod port_select; //////////////////////////////////////////////////////// diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index b07691ef..d565f33c 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -1,6 +1,5 @@ use crate::*; use super::*; -use KeyCode::Char; pub struct GrooveboxTui { pub sequencer: SequencerTui, pub sampler: SamplerTui, diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 16b62cab..1bddb4eb 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -1,6 +1,6 @@ use crate::*; use ClockCommand::{Play, Pause}; -use KeyCode::{Tab, BackTab, Char}; +use KeyCode::{Tab, Char}; use SequencerCommand::*; use PhraseCommand::*; use PhrasePoolCommand::*; diff --git a/crates/tek/src/tui/arranger_clip.rs b/crates/tek/src/tui/arranger_clip.rs index 9f61d1f1..0a0019be 100644 --- a/crates/tek/src/tui/arranger_clip.rs +++ b/crates/tek/src/tui/arranger_clip.rs @@ -1,7 +1,7 @@ use crate::*; pub fn to_arranger_clip_command (input: &TuiInput, t: usize, len_t: usize, s: usize, len_s: usize) -> Option { - use KeyCode::{Char, Up, Down, Left, Right, Delete}; + use KeyCode::{Char, Delete}; use ArrangerCommand as Cmd; use ArrangerSelection as Select; use ArrangerClipCommand as Clip; diff --git a/crates/tek/src/tui/arranger_scene.rs b/crates/tek/src/tui/arranger_scene.rs index b04ff695..71df8055 100644 --- a/crates/tek/src/tui/arranger_scene.rs +++ b/crates/tek/src/tui/arranger_scene.rs @@ -74,7 +74,7 @@ pub enum ArrangerSceneCommand { SetColor(usize, ItemPalette), } pub fn to_arranger_scene_command (input: &TuiInput, s: usize, len: usize) -> Option { - use KeyCode::{Char, Up, Down, Right, Enter, Delete}; + use KeyCode::{Char, Delete}; use ArrangerCommand as Cmd; use ArrangerSelection as Select; use ArrangerSceneCommand as Scene; diff --git a/crates/tek/src/tui/arranger_track.rs b/crates/tek/src/tui/arranger_track.rs index 41dee088..6d49f9e7 100644 --- a/crates/tek/src/tui/arranger_track.rs +++ b/crates/tek/src/tui/arranger_track.rs @@ -1,5 +1,5 @@ use crate::*; -use KeyCode::{Char, Down, Left, Right, Delete}; +use KeyCode::{Char, Delete}; pub trait HasTracks: Send + Sync { fn tracks (&self) -> &Vec; diff --git a/crates/tek/src/tui/file_browser.rs b/crates/tek/src/tui/file_browser.rs index 2cd49187..9911058d 100644 --- a/crates/tek/src/tui/file_browser.rs +++ b/crates/tek/src/tui/file_browser.rs @@ -1,7 +1,4 @@ use crate::*; -use KeyCode::{Up, Down, Right, Left, Enter, Esc, Char, Backspace}; -use FileBrowserCommand::*; -use super::phrase_list::PhraseListMode::{Import, Export}; /// Browses for phrase to import/export #[derive(Debug, Clone)] pub struct FileBrowser { diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 6de320af..21979c00 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -329,7 +329,7 @@ command!(|self: FileBrowserCommand, state: PhraseListModel|{ None }); input_to_command!(FileBrowserCommand:|state:PhraseListModel,from|{ - use PhraseListMode::*; + use FileBrowserCommand::*; use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char}; if let Some(PhraseListMode::Import(_index, browser)) = &state.mode { From f921260f6f58f0874ed45d417ef08909c97e60bb Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 19 Dec 2024 20:29:37 +0100 Subject: [PATCH 041/905] 8200s territory --- crates/tek/src/audio/mixer.rs | 10 +------ crates/tek/src/tui/app_transport.rs | 7 +---- crates/tek/src/tui/arranger_mode_v.rs | 39 +++++++++------------------ crates/tek/src/tui/phrase_list.rs | 14 +++++----- 4 files changed, 20 insertions(+), 50 deletions(-) diff --git a/crates/tek/src/audio/mixer.rs b/crates/tek/src/audio/mixer.rs index 8e5f0135..a0ac9366 100644 --- a/crates/tek/src/audio/mixer.rs +++ b/crates/tek/src/audio/mixer.rs @@ -1,5 +1,4 @@ use crate::*; - #[derive(Debug)] pub struct Mixer { /// JACK client handle (needs to not be dropped for standalone mode to work). @@ -9,15 +8,8 @@ pub struct Mixer { pub selected_track: usize, pub selected_column: usize, } - pub struct MixerAudio { model: Arc> } - -impl From<&Arc>> for MixerAudio { - fn from (model: &Arc>) -> Self { - Self { model: model.clone() } - } -} - +from!(|mode: &Arc>| MixerAudio = Self { model: model.clone() }); audio!(|self: MixerAudio, _, _|Control::Continue); diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index f368a621..7b00645d 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -147,12 +147,7 @@ pub enum TransportFocus { Clock, Quant, } - -impl From<&TransportTui> for Option { - fn from (state: &TransportTui) -> Self { - Some(state.focus.inner()) - } -} +from!(|state: &TransportTui|Option = Some(state.focus.inner())); impl FocusWrap for TransportFocus { fn wrap <'a, W: Render> (self, focus: TransportFocus, content: &'a W) diff --git a/crates/tek/src/tui/arranger_mode_v.rs b/crates/tek/src/tui/arranger_mode_v.rs index 1a6c4608..8056f3a2 100644 --- a/crates/tek/src/tui/arranger_mode_v.rs +++ b/crates/tek/src/tui/arranger_mode_v.rs @@ -1,5 +1,4 @@ use crate::*; - pub struct ArrangerVColSep { cols: Vec<(usize, usize)>, scenes_w: u16, @@ -19,21 +18,14 @@ render!(|self: ArrangerVColSep|render(move|to: &mut TuiOutput|{ } }) })); - pub struct ArrangerVRowSep { rows: Vec<(usize, usize)>, sep_fg: Color, } - -impl From<(&ArrangerTui, usize)> for ArrangerVRowSep { - fn from ((state, factor): (&ArrangerTui, usize)) -> Self { - Self { - rows: ArrangerScene::ppqs(state.scenes(), factor), - sep_fg: TuiTheme::separator_fg(false), - } - } -} - +from!(|args:(&ArrangerTui, usize)|ArrangerVRowSep = Self { + rows: ArrangerScene::ppqs(args.0.scenes(), args.1), + sep_fg: TuiTheme::separator_fg(false), +}); render!(|self: ArrangerVRowSep|render(move|to: &mut TuiOutput|{ Ok(for y in self.rows.iter().map(|row|row.1) { let y = to.area().y() + (y / PPQ) as u16 + 1; @@ -47,7 +39,6 @@ render!(|self: ArrangerVRowSep|render(move|to: &mut TuiOutput|{ } }) })); - pub struct ArrangerVCursor { cols: Vec<(usize, usize)>, rows: Vec<(usize, usize)>, @@ -56,20 +47,14 @@ pub struct ArrangerVCursor { scenes_w: u16, header_h: u16, } - -impl From<(&ArrangerTui, usize)> for ArrangerVCursor { - fn from ((state, factor): (&ArrangerTui, usize)) -> Self { - Self { - cols: track_widths(state.tracks()), - rows: ArrangerScene::ppqs(state.scenes(), factor), - focused: true, - selected: state.selected, - scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, - header_h: 3, - } - } -} - +from!(|args:(&ArrangerTui, usize)|ArrangerVCursor = Self { + cols: track_widths(state.tracks()), + rows: ArrangerScene::ppqs(args.0.scenes(), args.1), + focused: true, + selected: state.selected, + scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, + header_h: 3, +}); render!(|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{ let area = to.area(); let focused = self.focused; diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 21979c00..f8b58e8f 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -167,14 +167,12 @@ impl Default for PhraseListModel { } } } -impl From<&Arc>> for PhraseListModel { - fn from (phrase: &Arc>) -> Self { - let mut model = Self::default(); - model.phrases.push(phrase.clone()); - model.phrase.store(1, Relaxed); - model - } -} +from!(|phrase:<&Arc>>| PhraseListModel = { + let mut model = Self::default(); + model.phrases.push(phrase.clone()); + model.phrase.store(1, Relaxed); + model +}); has_phrases!(|self:PhraseListModel|self.phrases); has_phrase!(|self:PhraseListModel|self.phrases[self.phrase_index()]); impl PhraseListModel { From 48f83fa94d36233c5b93134994de38c0d9f6d102 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Fri, 20 Dec 2024 13:06:05 +0100 Subject: [PATCH 042/905] impl all froms (8263loc) --- crates/tek/src/core/color.rs | 58 ++++++++++++--------------- crates/tek/src/jack.rs | 27 +++++++++++-- crates/tek/src/jack/client.rs | 39 +++++++----------- crates/tek/src/jack/from_jack.rs | 13 ------ crates/tek/src/lib.rs | 12 ------ crates/tek/src/time/clock.rs | 34 ++++++++-------- crates/tek/src/tui.rs | 2 +- crates/tek/src/tui/app_arranger.rs | 16 ++++---- crates/tek/src/tui/app_transport.rs | 12 +++--- crates/tek/src/tui/arranger_mode_v.rs | 6 +-- crates/tek/src/tui/phrase_editor.rs | 26 +++++------- crates/tek/src/tui/phrase_list.rs | 2 +- crates/tek/src/tui/tui_output.rs | 6 +-- 13 files changed, 110 insertions(+), 143 deletions(-) delete mode 100644 crates/tek/src/jack/from_jack.rs diff --git a/crates/tek/src/core/color.rs b/crates/tek/src/core/color.rs index 0c96d043..93f086b6 100644 --- a/crates/tek/src/core/color.rs +++ b/crates/tek/src/core/color.rs @@ -50,41 +50,35 @@ impl ItemColor { self.okhsl.mix(other.okhsl, distance).into() } } -impl From for ItemPalette { - fn from (base: Color) -> Self { - Self::from(ItemColor::from(base)) - } -} -impl From for ItemPalette { - fn from (base: ItemColor) -> Self { - let mut light = base.okhsl.clone(); - light.lightness = (light.lightness * 1.3).min(Okhsl::::max_lightness()); - let mut lighter = light.clone(); - lighter.lightness = (lighter.lightness * 1.3).min(Okhsl::::max_lightness()); - let mut lightest = lighter.clone(); - lightest.lightness = (lightest.lightness * 1.3).min(Okhsl::::max_lightness()); +from!(|base: Color|ItemPalette = Self::from(ItemColor::from(base))); +from!(|base: ItemColor|ItemPalette = { + let mut light = base.okhsl.clone(); + light.lightness = (light.lightness * 1.3).min(Okhsl::::max_lightness()); + let mut lighter = light.clone(); + lighter.lightness = (lighter.lightness * 1.3).min(Okhsl::::max_lightness()); + let mut lightest = lighter.clone(); + lightest.lightness = (lightest.lightness * 1.3).min(Okhsl::::max_lightness()); - let mut dark = base.okhsl.clone(); - dark.lightness = (dark.lightness * 0.75).max(Okhsl::::min_lightness()); - dark.saturation = (dark.saturation * 0.75).max(Okhsl::::min_saturation()); - let mut darker = dark.clone(); - darker.lightness = (darker.lightness * 0.66).max(Okhsl::::min_lightness()); - darker.saturation = (darker.saturation * 0.66).max(Okhsl::::min_saturation()); - let mut darkest = darker.clone(); - darkest.lightness = (darkest.lightness * 0.50).max(Okhsl::::min_lightness()); - darkest.saturation = (darkest.saturation * 0.50).max(Okhsl::::min_saturation()); + let mut dark = base.okhsl.clone(); + dark.lightness = (dark.lightness * 0.75).max(Okhsl::::min_lightness()); + dark.saturation = (dark.saturation * 0.75).max(Okhsl::::min_saturation()); + let mut darker = dark.clone(); + darker.lightness = (darker.lightness * 0.66).max(Okhsl::::min_lightness()); + darker.saturation = (darker.saturation * 0.66).max(Okhsl::::min_saturation()); + let mut darkest = darker.clone(); + darkest.lightness = (darkest.lightness * 0.50).max(Okhsl::::min_lightness()); + darkest.saturation = (darkest.saturation * 0.50).max(Okhsl::::min_saturation()); - Self { - base, - light: light.into(), - lighter: lighter.into(), - lightest: lightest.into(), - dark: dark.into(), - darker: darker.into(), - darkest: darkest.into(), - } + Self { + base, + light: light.into(), + lighter: lighter.into(), + lightest: lightest.into(), + dark: dark.into(), + darker: darker.into(), + darkest: darkest.into(), } -} +}); impl ItemPalette { pub fn random () -> Self { ItemColor::random().into() diff --git a/crates/tek/src/jack.rs b/crates/tek/src/jack.rs index 44332681..0dd51dc1 100644 --- a/crates/tek/src/jack.rs +++ b/crates/tek/src/jack.rs @@ -1,10 +1,31 @@ - -pub(crate) mod activate; +pub use ::jack as libjack; +pub(crate) mod activate; pub(crate) use self::activate::*; pub(crate) mod audio; pub(crate) use self::audio::*; pub(crate) mod client; pub(crate) use self::client::*; -pub(crate) mod from_jack; pub(crate) mod jack_event; pub(crate) use self::jack_event::*; pub(crate) mod ports; pub(crate) use self::ports::*; +pub(crate) use ::jack::{ + contrib::ClosureProcessHandler, + Client, AsyncClient, ClientOptions, ClientStatus, + ProcessScope, Control, CycleTimes, + Port, PortId, + PortSpec, MidiIn, MidiOut, AudioIn, AudioOut, Unowned, + Transport, TransportState, MidiIter, RawMidi, + Frames, + NotificationHandler, +}; + +/// Implement [TryFrom<&Arc>>]: create app state from wrapped JACK handle. +#[macro_export] macro_rules! from_jack { + (|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc>> for $Struct $(<$($L),*$($T),*>)? { + type Error = Box; + fn try_from ($jack: &Arc>) -> Usually { + Ok($cb) + } + } + }; +} //////////////////////////////////////////////////////////////////////////////////// diff --git a/crates/tek/src/jack/client.rs b/crates/tek/src/jack/client.rs index 046990bd..be18cb0f 100644 --- a/crates/tek/src/jack/client.rs +++ b/crates/tek/src/jack/client.rs @@ -1,5 +1,7 @@ use crate::*; - +pub type DynamicAsyncClient = AsyncClient; +pub type DynamicAudioHandler = ClosureProcessHandler<(), BoxedAudioHandler>; +pub type BoxedAudioHandler = Box Control + Send>; /// Wraps [Client] or [DynamicAsyncClient] in place. #[derive(Debug)] pub enum JackClient { @@ -10,7 +12,17 @@ pub enum JackClient { /// After activation. Must not be dropped for JACK thread to persist. Active(DynamicAsyncClient), } - +from!(|jack: JackClient|Client = match jack { + JackClient::Inactive(client) => client, + JackClient::Activating => panic!("jack client still activating"), + JackClient::Active(_) => panic!("jack client already activated"), +}); +impl JackClient { + pub fn new (name: &str) -> Usually { + let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; + Ok(Self::Inactive(client)) + } +} impl AudioEngine for JackClient { fn client(&self) -> &Client { match self { @@ -36,26 +48,3 @@ impl AudioEngine for JackClient { Ok(state) } } - -pub type DynamicAsyncClient = AsyncClient; - -pub type DynamicAudioHandler = ClosureProcessHandler<(), BoxedAudioHandler>; - -pub type BoxedAudioHandler = Box Control + Send>; - -impl JackClient { - pub fn new (name: &str) -> Usually { - let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; - Ok(Self::Inactive(client)) - } -} - -impl From for Client { - fn from (jack: JackClient) -> Client { - match jack { - JackClient::Inactive(client) => client, - JackClient::Activating => panic!("jack client still activating"), - JackClient::Active(_) => panic!("jack client already activated"), - } - } -} diff --git a/crates/tek/src/jack/from_jack.rs b/crates/tek/src/jack/from_jack.rs deleted file mode 100644 index bdd1fd2e..00000000 --- a/crates/tek/src/jack/from_jack.rs +++ /dev/null @@ -1,13 +0,0 @@ - - -/// Implement [TryFrom<&Arc>>]: create app state from wrapped JACK handle. -#[macro_export] macro_rules! from_jack { - (|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc>> for $Struct $(<$($L),*$($T),*>)? { - type Error = Box; - fn try_from ($jack: &Arc>) -> Usually { - Ok($cb) - } - } - }; -} diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs index a13ac2f9..4cf3080e 100644 --- a/crates/tek/src/lib.rs +++ b/crates/tek/src/lib.rs @@ -65,18 +65,6 @@ pub(crate) use ratatui::{ backend::{Backend, CrosstermBackend, ClearType} }; -pub use ::jack as libjack; -pub(crate) use ::jack::{ - contrib::ClosureProcessHandler, - Client, AsyncClient, ClientOptions, ClientStatus, - ProcessScope, Control, CycleTimes, - Port, PortId, - PortSpec, MidiIn, MidiOut, AudioOut, Unowned, - Transport, TransportState, MidiIter, RawMidi, - Frames, - NotificationHandler, -}; - pub use ::midly; pub(crate) use ::midly::{ Smf, diff --git a/crates/tek/src/time/clock.rs b/crates/tek/src/time/clock.rs index 8636852a..e9c0d2d2 100644 --- a/crates/tek/src/time/clock.rs +++ b/crates/tek/src/time/clock.rs @@ -63,25 +63,23 @@ pub struct ClockModel { pub chunk: Arc, } -impl From<&Arc>> for ClockModel { - fn from (jack: &Arc>) -> Self { - let jack = jack.read().unwrap(); - let chunk = jack.client().buffer_size(); - let transport = jack.client().transport(); - let timebase = Arc::new(Timebase::default()); - Self { - quant: Arc::new(24.into()), - sync: Arc::new(384.into()), - transport: Arc::new(transport), - chunk: Arc::new((chunk as usize).into()), - global: Arc::new(Moment::zero(&timebase)), - playhead: Arc::new(Moment::zero(&timebase)), - offset: Arc::new(Moment::zero(&timebase)), - started: RwLock::new(None).into(), - timebase, - } +from!(|jack: &Arc>| ClockModel = { + let jack = jack.read().unwrap(); + let chunk = jack.client().buffer_size(); + let transport = jack.client().transport(); + let timebase = Arc::new(Timebase::default()); + Self { + quant: Arc::new(24.into()), + sync: Arc::new(384.into()), + transport: Arc::new(transport), + chunk: Arc::new((chunk as usize).into()), + global: Arc::new(Moment::zero(&timebase)), + playhead: Arc::new(Moment::zero(&timebase)), + offset: Arc::new(Moment::zero(&timebase)), + started: RwLock::new(None).into(), + timebase, } -} +}); impl std::fmt::Debug for ClockModel { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index 787d40e2..9f231045 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -21,7 +21,7 @@ pub(crate) use tui_border::*; mod app_transport; pub(crate) use app_transport::*; mod app_sequencer; pub(crate) use app_sequencer::*; mod app_sampler; pub(crate) use app_sampler::*; -mod app_groovebox; +mod app_groovebox; pub(crate) use app_groovebox::*; mod app_arranger; pub(crate) use app_arranger::*; /////////////////////////////////////////////////////// diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 90fd03ea..470a1391 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -27,8 +27,8 @@ from_jack!(|jack| ArrangerTui { ))); Self { clock, - phrases: PhraseListModel::from(&phrase), - editor: PhraseEditorModel::from(&phrase), + phrases: (&phrase).into(), + editor: (&phrase).into(), selected: ArrangerSelection::Clip(0, 0), scenes: vec![], tracks: vec![], @@ -282,13 +282,13 @@ fn any_size (_: E::Size) -> Perhaps{ Ok(Some([0.into(),0.into()].into())) } impl ArrangerTui { - fn selected (&self) -> ArrangerSelection { + pub fn selected (&self) -> ArrangerSelection { self.selected } - fn selected_mut (&mut self) -> &mut ArrangerSelection { + pub fn selected_mut (&mut self) -> &mut ArrangerSelection { &mut self.selected } - fn activate (&mut self) -> Usually<()> { + pub fn activate (&mut self) -> Usually<()> { if let ArrangerSelection::Scene(s) = self.selected { for (t, track) in self.tracks.iter_mut().enumerate() { let phrase = self.scenes[s].clips[t].clone(); @@ -305,15 +305,15 @@ impl ArrangerTui { }; Ok(()) } - fn selected_phrase (&self) -> Option>> { + pub fn selected_phrase (&self) -> Option>> { self.selected_scene()?.clips.get(self.selected.track()?)?.clone() } - fn toggle_loop (&mut self) { + pub fn toggle_loop (&mut self) { if let Some(phrase) = self.selected_phrase() { phrase.write().unwrap().toggle_loop() } } - fn randomize_color (&mut self) { + pub fn randomize_color (&mut self) { match self.selected { ArrangerSelection::Mix => { self.color = ItemPalette::random() diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index 7b00645d..7c058152 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -9,14 +9,14 @@ pub struct TransportTui { pub clock: ClockModel, pub size: Measure, pub cursor: (usize, usize), - pub focus: FocusState, + pub focus: TransportFocus, } from_jack!(|jack|TransportTui Self { jack: jack.clone(), clock: ClockModel::from(jack), size: Measure::new(), cursor: (0, 0), - focus: FocusState::Entered(TransportFocus::PlayPause) + focus: TransportFocus::PlayPause }); has_clock!(|self:TransportTui|&self.clock); audio!(|self:TransportTui,client,scope|ClockAudio(self).process(client, scope)); @@ -131,10 +131,10 @@ render!(|self: PlayPause|Tui::bg( impl HasFocus for TransportTui { type Item = TransportFocus; fn focused (&self) -> Self::Item { - self.focus.inner() + self.focus } fn set_focused (&mut self, to: Self::Item) { - self.focus.set_inner(to) + self.focus = to } } @@ -147,8 +147,6 @@ pub enum TransportFocus { Clock, Quant, } -from!(|state: &TransportTui|Option = Some(state.focus.inner())); - impl FocusWrap for TransportFocus { fn wrap <'a, W: Render> (self, focus: TransportFocus, content: &'a W) -> impl Render + 'a @@ -177,7 +175,7 @@ pub trait TransportControl: HasClock + { impl TransportControl for TransportTui { fn transport_focused (&self) -> Option { - Some(self.focus.inner()) + Some(self.focus) } } diff --git a/crates/tek/src/tui/arranger_mode_v.rs b/crates/tek/src/tui/arranger_mode_v.rs index 8056f3a2..d4980ece 100644 --- a/crates/tek/src/tui/arranger_mode_v.rs +++ b/crates/tek/src/tui/arranger_mode_v.rs @@ -48,11 +48,11 @@ pub struct ArrangerVCursor { header_h: u16, } from!(|args:(&ArrangerTui, usize)|ArrangerVCursor = Self { - cols: track_widths(state.tracks()), + cols: track_widths(args.0.tracks()), rows: ArrangerScene::ppqs(args.0.scenes(), args.1), focused: true, - selected: state.selected, - scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, + selected: args.0.selected(), + scenes_w: 3 + ArrangerScene::longest_name(args.0.scenes()) as u16, header_h: 3, }); render!(|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{ diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index d78745e7..bcbaa652 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -207,22 +207,18 @@ impl PhraseEditorModel { } } -impl From<&Arc>> for PhraseEditorModel { - fn from (phrase: &Arc>) -> Self { - let mut model = Self::from(Some(phrase.clone())); - model.redraw(); - model - } -} +from!(|phrase: &Arc>|PhraseEditorModel = { + let mut model = Self::from(Some(phrase.clone())); + model.redraw(); + model +}); -impl From>>> for PhraseEditorModel { - fn from (phrase: Option>>) -> Self { - let mut model = Self::default(); - *model.phrase_mut() = phrase; - model.redraw(); - model - } -} +from!(|phrase: Option>>|PhraseEditorModel = { + let mut model = Self::default(); + *model.phrase_mut() = phrase; + model.redraw(); + model +}); impl std::fmt::Debug for PhraseEditorModel { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index f8b58e8f..5d211284 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -167,7 +167,7 @@ impl Default for PhraseListModel { } } } -from!(|phrase:<&Arc>>| PhraseListModel = { +from!(|phrase:&Arc>|PhraseListModel = { let mut model = Self::default(); model.phrases.push(phrase.clone()); model.phrase.store(1, Relaxed); diff --git a/crates/tek/src/tui/tui_output.rs b/crates/tek/src/tui/tui_output.rs index 6e4070c0..b346b535 100644 --- a/crates/tek/src/tui/tui_output.rs +++ b/crates/tek/src/tui/tui_output.rs @@ -91,11 +91,7 @@ impl BigBuffer { } } -impl From<(usize, usize)> for BigBuffer { // cuteness overload - fn from ((width, height): (usize, usize)) -> Self { - Self::new(width, height) - } -} +from!(|size:(usize, usize)| BigBuffer = Self::new(size.0, size.1)); pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) { for row in 0..area.h() { From 99d8a0863e82c7a69466fd9f6e8bc42fb267148f Mon Sep 17 00:00:00 2001 From: unspeaker Date: Fri, 20 Dec 2024 23:15:48 +0100 Subject: [PATCH 043/905] refactor arranger header --- crates/tek/src/tui/app_groovebox.rs | 7 ++ crates/tek/src/tui/arranger_mode_v.rs | 156 ++++++++++++++------------ 2 files changed, 93 insertions(+), 70 deletions(-) diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index d565f33c..92241815 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -9,6 +9,13 @@ pub struct GrooveboxTui { from_jack!(|jack|GrooveboxTui { let mut sequencer = SequencerTui::try_from(jack)?; sequencer.status = false; + let midi_in_1 = jack.read().unwrap().register_port("in1", MidiIn::default())?; + let midi_out = jack.read().unwrap().register_port("out", MidiOut::default())?; + let midi_in_2 = jack.read().unwrap().register_port("in2", MidiIn::default())?; + let audio_in_1 = jack.read().unwrap().register_port("inL", AudioIn::default())?; + let audio_in_2 = jack.read().unwrap().register_port("inR", AudioIn::default())?; + let audio_out_1 = jack.read().unwrap().register_port("out1", AudioOut::default())?; + let audio_out_2 = jack.read().unwrap().register_port("out2", AudioOut::default())?; Self { sequencer, sampler: SamplerTui::try_from(jack)?, diff --git a/crates/tek/src/tui/arranger_mode_v.rs b/crates/tek/src/tui/arranger_mode_v.rs index d4980ece..354b4260 100644 --- a/crates/tek/src/tui/arranger_mode_v.rs +++ b/crates/tek/src/tui/arranger_mode_v.rs @@ -1,4 +1,90 @@ use crate::*; +pub struct ArrangerVHead<'a> { + focused: bool, + selected: ArrangerSelection, + scenes_w: u16, + header_h: u16, + timebase: &'a Arc, + current: &'a Arc, + tracks: Vec<(&'a ArrangerTrack, usize, usize)>, +} +from!(<'a>|state: &'a ArrangerTui|ArrangerVHead<'a> = Self { // A + focused: true, + selected: state.selected, + scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, + header_h: 3, + timebase: state.clock().timebase(), + current: &state.clock().playhead, + tracks: track_widths(state.tracks()) + .iter() + .enumerate() + .map(|(i, (a, b))|(&state.tracks()[i], *a, *b)) + .collect(), +}); +render!(|self: ArrangerVHead<'a>|row!( + (track, w, _) in self.tracks.iter() => { + let name = Self::format_name(track, *w); + let input = Self::format_input(track)?; + let elapsed = Self::format_elapsed(track, &self.timebase); + let until_next = Self::format_until_next(track, &self.timebase, &self.current); + let output = Self::format_output(track)?; + Tui::push_x(self.scenes_w, + Tui::bg(track.color().base.rgb, + Tui::min_xy(*w as u16, self.header_h, row!([ + col!(!["▎", "▎", "▎", "▎", "▎", "▎",]), + col!(![name, input, output, elapsed, until_next, output])])))) + } +)); +impl<'a> ArrangerVHead<'a> { + /// name and width of track + fn format_name (track: &ArrangerTrack, w: usize) -> impl Render { + let name = track.name().read().unwrap(); + let max_w = w.saturating_sub(1).min(name.len()).max(2); + Tui::bold(true, Tui::fg(track.color.lightest.rgb, format!("▎{}", &name[0..max_w]))); + } + /// input port + fn format_input (track: &ArrangerTrack) -> Usually> { + Ok(format!(">{}", track.player.midi_ins().get(0) + .map(|port|port.short_name()) + .transpose()? + .unwrap_or("(none)".into()))) + } + /// output port + fn format_output (track: &ArrangerTrack) -> Usually> { + Ok(format!("<{}", track.player.midi_outs().get(0) + .map(|port|port.short_name()) + .transpose()? + .unwrap_or("(none)".into()))) + } + /// beats elapsed + fn format_elapsed (track: &ArrangerTrack, timebase: &Arc) -> impl Render { + if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() { + let length = phrase.read().unwrap().length; + let elapsed = track.player.pulses_since_start().unwrap(); + let elapsed = timebase.format_beats_1_short( + (elapsed as usize % length) as f64 + ); + format!("+{elapsed:>}") + } else { + String::new() + } + } + /// beats until switchover + fn format_until_next (track: &ArrangerTrack, timebase: &Arc, current: &Arc) + -> Option> + { + track.player.next_phrase().as_ref().map(|(t, _)|{ + let target = t.pulse.get(); + let current = current.pulse.get(); + if target > current { + let remaining = target - current; + format!("-{:>}", timebase.format_beats_0_short(remaining)) + } else { + String::new() + } + }) + } +} pub struct ArrangerVColSep { cols: Vec<(usize, usize)>, scenes_w: u16, @@ -114,76 +200,6 @@ render!(|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{ }) })); -pub struct ArrangerVHead<'a> { - tracks: &'a Vec, - cols: Vec<(usize, usize)>, - focused: bool, - selected: ArrangerSelection, - scenes_w: u16, - header_h: u16, - timebase: &'a Arc, - current: &'a Arc, -} -from!(<'a>|state: &'a ArrangerTui|ArrangerVHead<'a> = Self { // A - tracks: &state.tracks, - cols: track_widths(state.tracks()), - focused: true, - selected: state.selected, - scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, - header_h: 3, - timebase: state.clock().timebase(), - current: &state.clock().playhead, -}); -render!(|self: ArrangerVHead<'a>|row!( - (track, w) in self.tracks.iter().zip(self.cols.iter().map(|col|col.0)) => { - - // name and width of track - let name = track.name().read().unwrap(); - let max_w = w.saturating_sub(1).min(name.len()).max(2); - let name = Tui::bold(true, Tui::fg(track.color.lightest.rgb, format!("▎{}", &name[0..max_w]))); - - // name of active MIDI input - let input = format!(">{}", track.player.midi_ins().get(0) - .map(|port|port.short_name()) - .transpose()? - .unwrap_or("(none)".into())); - - // beats elapsed - let elapsed = if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() { - let length = phrase.read().unwrap().length; - let elapsed = track.player.pulses_since_start().unwrap(); - let elapsed = self.timebase.format_beats_1_short( - (elapsed as usize % length) as f64 - ); - format!("+{elapsed:>}") - } else { - String::new() - }; - - // beats until switchover - let until_next = track.player.next_phrase().as_ref().map(|(t, _)|{ - let target = t.pulse.get(); - let current = self.current.pulse.get(); - if target > current { - let remaining = target - current; - format!("-{:>}", self.timebase.format_beats_0_short(remaining)) - } else { - String::new() - } - }); - - // name of active MIDI output - let output = format!("<{}", track.player.midi_outs().get(0) - .map(|port|port.short_name()) - .transpose()? - .unwrap_or("(none)".into())); - Tui::push_x(self.scenes_w, - Tui::bg(track.color().base.rgb, - Tui::min_xy(w as u16, self.header_h, row!([ - col!(!["▎", "▎", "▎", "▎", "▎", "▎",]), - col!(![name, input, output, elapsed, until_next, output])])))) - } -)); pub struct ArrangerVBody<'a> { size: &'a Measure, From 598319af35a9af177a4dbfbea7ef8bb4b16a25be Mon Sep 17 00:00:00 2001 From: unspeaker Date: Fri, 20 Dec 2024 23:45:55 +0100 Subject: [PATCH 044/905] more arranger view refactors --- crates/tek/src/core.rs | 4 +- crates/tek/src/tui/arranger_mode_v.rs | 184 ++++++++++++-------------- 2 files changed, 90 insertions(+), 98 deletions(-) diff --git a/crates/tek/src/core.rs b/crates/tek/src/core.rs index 621074be..f8906185 100644 --- a/crates/tek/src/core.rs +++ b/crates/tek/src/core.rs @@ -14,8 +14,8 @@ pub use self::{ /// Prototypal case of implementor macro. /// Saves 4loc per data pats. #[macro_export] macro_rules! from { - ($(<$lt:lifetime>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => { - impl $(<$lt>)? From<$Source> for $Target { + ($(<$($lt:lifetime),+>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => { + impl $(<$($lt),+>)? From<$Source> for $Target { fn from ($state:$Source) -> Self { $cb } } }; diff --git a/crates/tek/src/tui/arranger_mode_v.rs b/crates/tek/src/tui/arranger_mode_v.rs index 354b4260..f8d54086 100644 --- a/crates/tek/src/tui/arranger_mode_v.rs +++ b/crates/tek/src/tui/arranger_mode_v.rs @@ -1,60 +1,68 @@ use crate::*; +const HEADER_H: u16 = 3; +fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { + let mut widths = vec![]; + let mut total = 0; + for track in tracks.iter() { + let width = track.width; + widths.push((width, total)); + total += width; + } + widths.push((0, total)); + widths +} +fn tracks_with_widths (tracks: &[ArrangerTrack]) + -> impl Iterator +{ + let mut x = 0; + tracks.iter().enumerate().map(move |(index, track)|{ + let data = (index, track, x, x + track.width); + x += track.width; + data + }) +} pub struct ArrangerVHead<'a> { - focused: bool, - selected: ArrangerSelection, scenes_w: u16, - header_h: u16, timebase: &'a Arc, current: &'a Arc, - tracks: Vec<(&'a ArrangerTrack, usize, usize)>, + tracks: &'a [ArrangerTrack], } from!(<'a>|state: &'a ArrangerTui|ArrangerVHead<'a> = Self { // A - focused: true, - selected: state.selected, - scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, - header_h: 3, + tracks: state.tracks(), timebase: state.clock().timebase(), current: &state.clock().playhead, - tracks: track_widths(state.tracks()) - .iter() - .enumerate() - .map(|(i, (a, b))|(&state.tracks()[i], *a, *b)) - .collect(), + scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, }); -render!(|self: ArrangerVHead<'a>|row!( - (track, w, _) in self.tracks.iter() => { - let name = Self::format_name(track, *w); - let input = Self::format_input(track)?; - let elapsed = Self::format_elapsed(track, &self.timebase); - let until_next = Self::format_until_next(track, &self.timebase, &self.current); - let output = Self::format_output(track)?; - Tui::push_x(self.scenes_w, - Tui::bg(track.color().base.rgb, - Tui::min_xy(*w as u16, self.header_h, row!([ - col!(!["▎", "▎", "▎", "▎", "▎", "▎",]), - col!(![name, input, output, elapsed, until_next, output])])))) +render!(|self: ArrangerVHead<'a>|Tui::push_x(self.scenes_w, row!( + (_, track, x1, x2) in tracks_with_widths(self.tracks) => { + let (w, h) = (x2 - x1, HEADER_H); + let color = track.color(); + Tui::bg(color.base.rgb, Tui::min_xy(w as u16, h, Fixed::wh(w as u16, 5, col!([ + row!(![Tui::fg(color.light.rgb, "▎"), Self::format_name(track, w)]), + row!(![Tui::fg(color.light.rgb, "▎"), Self::format_input(track)?]), + row!(![Tui::fg(color.light.rgb, "▎"), Self::format_output(track)?]), + row!(![Tui::fg(color.light.rgb, "▎"), Self::format_elapsed(track, &self.timebase)]), + row!(![Tui::fg(color.light.rgb, "▎"), Self::format_until_next(track, &self.current)]), + ])))) } -)); +))); impl<'a> ArrangerVHead<'a> { /// name and width of track fn format_name (track: &ArrangerTrack, w: usize) -> impl Render { let name = track.name().read().unwrap(); let max_w = w.saturating_sub(1).min(name.len()).max(2); - Tui::bold(true, Tui::fg(track.color.lightest.rgb, format!("▎{}", &name[0..max_w]))); + let name = String::from(&name[0..max_w]); + Tui::bold(true, Tui::fg(track.color.lightest.rgb, name)); } /// input port fn format_input (track: &ArrangerTrack) -> Usually> { - Ok(format!(">{}", track.player.midi_ins().get(0) - .map(|port|port.short_name()) - .transpose()? - .unwrap_or("(none)".into()))) + Ok(format!(">{}", track.player.midi_ins().get(0).map(|port|port.short_name()) + .transpose()?.unwrap_or("?".into()))) } /// output port fn format_output (track: &ArrangerTrack) -> Usually> { - Ok(format!("<{}", track.player.midi_outs().get(0) - .map(|port|port.short_name()) - .transpose()? - .unwrap_or("(none)".into()))) + Ok(format!("<{}", track.player.midi_outs().get(0).map(|port|port.short_name()) + .transpose()?.unwrap_or("?".into()))) } /// beats elapsed fn format_elapsed (track: &ArrangerTrack, timebase: &Arc) -> impl Render { @@ -70,9 +78,10 @@ impl<'a> ArrangerVHead<'a> { } } /// beats until switchover - fn format_until_next (track: &ArrangerTrack, timebase: &Arc, current: &Arc) + fn format_until_next (track: &ArrangerTrack, current: &Arc) -> Option> { + let timebase = ¤t.timebase; track.player.next_phrase().as_ref().map(|(t, _)|{ let target = t.pulse.get(); let current = current.pulse.get(); @@ -128,22 +137,18 @@ render!(|self: ArrangerVRowSep|render(move|to: &mut TuiOutput|{ pub struct ArrangerVCursor { cols: Vec<(usize, usize)>, rows: Vec<(usize, usize)>, - focused: bool, selected: ArrangerSelection, scenes_w: u16, - header_h: u16, } from!(|args:(&ArrangerTui, usize)|ArrangerVCursor = Self { cols: track_widths(args.0.tracks()), rows: ArrangerScene::ppqs(args.0.scenes(), args.1), - focused: true, selected: args.0.selected(), scenes_w: 3 + ArrangerScene::longest_name(args.0.scenes()) as u16, - header_h: 3, }); render!(|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{ let area = to.area(); - let focused = self.focused; + let focused = true; let selected = self.selected; let get_track_area = |t: usize| [ self.scenes_w + area.x() + self.cols[t].1 as u16, @@ -153,13 +158,13 @@ render!(|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{ ]; let get_scene_area = |s: usize| [ area.x(), - self.header_h + area.y() + (self.rows[s].1 / PPQ) as u16, + HEADER_H + area.y() + (self.rows[s].1 / PPQ) as u16, area.w(), (self.rows[s].0 / PPQ) as u16 ]; let get_clip_area = |t: usize, s: usize| [ self.scenes_w + area.x() + self.cols[t].1 as u16, - self.header_h + area.y() + (self.rows[s].1/PPQ) as u16, + HEADER_H + area.y() + (self.rows[s].1/PPQ) as u16, self.cols[t].0 as u16, (self.rows[s].0 / PPQ) as u16 ]; @@ -194,78 +199,65 @@ render!(|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{ } Ok(if focused { to.render_in(if let Some(clip_area) = clip_area { clip_area } - else if let Some(track_area) = track_area { track_area.clip_h(self.header_h) } + else if let Some(track_area) = track_area { track_area.clip_h(HEADER_H) } else if let Some(scene_area) = scene_area { scene_area.clip_w(self.scenes_w) } - else { area.clip_w(self.scenes_w).clip_h(self.header_h) }, &CORNERS)? + else { area.clip_w(self.scenes_w).clip_h(HEADER_H) }, &CORNERS)? }) })); - pub struct ArrangerVBody<'a> { size: &'a Measure, scenes: &'a Vec, tracks: &'a Vec, rows: Vec<(usize, usize)>, - cols: Vec<(usize, usize)>, - header_h: u16, } - -impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVBody<'a> { - fn from ((state, factor): (&'a ArrangerTui, usize)) -> Self { - Self { - size: &state.size, - scenes: &state.scenes, - tracks: &state.tracks, - rows: ArrangerScene::ppqs(state.scenes(), factor), - cols: track_widths(state.tracks()), - header_h: 3, - } - } -} - +from!(<'a>|args:(&'a ArrangerTui, usize)|ArrangerVBody<'a> = Self { + size: &args.0.size, + scenes: &args.0.scenes, + tracks: &args.0.tracks, + rows: ArrangerScene::ppqs(args.0.scenes(), args.1), +}); render!(|self: ArrangerVBody<'a>|Fixed::h( - (self.size.h() as u16).saturating_sub(self.header_h), + (self.size.h() as u16).saturating_sub(HEADER_H), col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => { let height = 1.max((pulses / PPQ) as u16); let playing = scene.is_playing(self.tracks); Fixed::h(height, row!([ - if playing { "▶ " } else { " " }, + Self::format_play(playing), Tui::bold(true, scene.name.read().unwrap().as_str()), - row!((track, w) in self.cols.iter().map(|col|col.0).enumerate() => { - Fixed::wh(w as u16, height, Layers::new(move |add|{ - let mut bg = TuiTheme::border_bg(); - match (self.tracks.get(track), scene.clips.get(track)) { - (Some(track), Some(Some(phrase))) => { - let name = &(phrase as &Arc>).read().unwrap().name; - let name = format!("{}", name); - let max_w = name.len().min((w as usize).saturating_sub(2)); - let color = phrase.read().unwrap().color; - bg = color.dark.rgb; - if let Some((_, Some(ref playing))) = track.player.play_phrase() { - if *playing.read().unwrap() == *phrase.read().unwrap() { - bg = color.light.rgb - } - }; - add(&Fixed::w(w as u16, Tui::push_x(1, &name.as_str()[0..max_w])))?; - }, - _ => {} - }; - //add(&Background(bg)) - Ok(()) - })) + row!((index, _, x1, x2) in tracks_with_widths(self.tracks) => { + Self::format_clip(&self.tracks, &scene, index, (x2 - x1) as u16, height) })]) ) }) )); - -fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { - let mut widths = vec![]; - let mut total = 0; - for track in tracks.iter() { - let width = track.width; - widths.push((width, total)); - total += width; +impl<'a> ArrangerVBody<'a> { + fn format_play (playing: bool) -> &'static str { + if playing { "▶ " } else { " " } + } + fn format_clip (tracks: &'a [ArrangerTrack], scene: &'a ArrangerScene, i: usize, w: u16, h: u16) + -> impl Render + use<'a> + { + Fixed::wh(w, h, Layers::new(move |add|{ + let mut bg = TuiTheme::border_bg(); + match (tracks.get(i), scene.clips.get(i)) { + (Some(track), Some(Some(phrase))) => { + let name = &(phrase as &Arc>).read().unwrap().name; + let name = format!("{}", name); + let max_w = name.len().min((w as usize).saturating_sub(2)); + let color = phrase.read().unwrap().color; + bg = color.dark.rgb; + if let Some((_, Some(ref playing))) = track.player.play_phrase() { + if *playing.read().unwrap() == *phrase.read().unwrap() { + bg = color.light.rgb + } + }; + add(&Fixed::w(w as u16, Tui::push_x(1, &name.as_str()[0..max_w])))?; + }, + _ => {} + }; + //add(&Background(bg)) + Ok(()) + })) } - widths.push((0, total)); - widths } From 53f786543d98b6115fba994eb969a3aaad8a43b2 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 21 Dec 2024 00:00:33 +0100 Subject: [PATCH 045/905] add Gettable, Mutable, InteriorMutable --- crates/tek/src/core.rs | 25 ++++++++++++ crates/tek/src/lib.rs | 26 +----------- crates/tek/src/tui/arranger_track.rs | 61 ++++++++++++++-------------- 3 files changed, 58 insertions(+), 54 deletions(-) diff --git a/crates/tek/src/core.rs b/crates/tek/src/core.rs index f8906185..0a469b98 100644 --- a/crates/tek/src/core.rs +++ b/crates/tek/src/core.rs @@ -1,3 +1,5 @@ +pub(crate) use std::error::Error; + pub(crate) mod color; pub(crate) use color::*; pub(crate) mod command; pub(crate) use command::*; pub(crate) mod engine; pub(crate) use engine::*; @@ -20,3 +22,26 @@ pub use self::{ } }; } + +pub trait Gettable { + fn get (&self) -> T; +} + +pub trait Mutable: Gettable { + fn set (&mut self, value: T); +} + +pub trait InteriorMutable: Gettable { + fn set (&self, value: T); +} + +/// Standard result type. +pub type Usually = Result>; + +/// Standard optional result type. +pub type Perhaps = Result, Box>; + +/// Define test modules. +#[macro_export] macro_rules! testmod { + ($($name:ident)*) => { $(#[cfg(test)] mod $name;)* }; +} diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs index 4cf3080e..ff4ffc18 100644 --- a/crates/tek/src/lib.rs +++ b/crates/tek/src/lib.rs @@ -8,29 +8,6 @@ pub mod midi; pub(crate) use self::midi::*; pub mod audio; pub(crate) use self::audio::*; //pub mod plugin; pub(crate) use self::plugin::*; -/// Standard result type. -pub type Usually = Result>; - -/// Standard optional result type. -pub type Perhaps = Result, Box>; - -/// Define and reexport submodules. -#[macro_export] macro_rules! submod { - ($($name:ident)*) => { $(mod $name; pub use self::$name::*;)* }; -} - -/// Define public modules. -#[macro_export] macro_rules! pubmod { - ($($name:ident)*) => { $(pub mod $name;)* }; -} - -/// Define test modules. -#[macro_export] macro_rules! testmod { - ($($name:ident)*) => { $(#[cfg(test)] mod $name;)* }; -} - -testmod! { test } - pub(crate) use clap::{self, Parser}; pub use ::better_panic; @@ -48,7 +25,6 @@ pub(crate) use std::path::PathBuf; pub(crate) use std::ffi::OsString; pub(crate) use std::time::Duration; pub(crate) use std::io::{Stdout, stdout}; -pub(crate) use std::error::Error; pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem}; pub(crate) use std::cmp::{Ord, Eq, PartialEq}; pub(crate) use std::fmt::{Debug, Display}; @@ -80,3 +56,5 @@ pub(crate) use ::palette::{ convert::*, okhsl::* }; + +testmod! { test } diff --git a/crates/tek/src/tui/arranger_track.rs b/crates/tek/src/tui/arranger_track.rs index 6d49f9e7..45e93081 100644 --- a/crates/tek/src/tui/arranger_track.rs +++ b/crates/tek/src/tui/arranger_track.rs @@ -1,6 +1,37 @@ use crate::*; use KeyCode::{Char, Delete}; +#[derive(Debug)] pub struct ArrangerTrack { + /// Name of track + pub(crate) name: Arc>, + /// Preferred width of track column + pub(crate) width: usize, + /// Identifying color of track + pub(crate) color: ItemPalette, + /// MIDI player state + pub(crate) player: PhrasePlayerModel, +} +has_clock!(|self:ArrangerTrack|self.player.clock()); +has_player!(|self:ArrangerTrack|self.player); +impl ArrangerTrackApi for ArrangerTrack { + /// Name of track + fn name (&self) -> &Arc> { + &self.name + } + /// Preferred width of track column + fn width (&self) -> usize { + self.width + } + /// Preferred width of track column + fn width_mut (&mut self) -> &mut usize { + &mut self.width + } + /// Identifying color of track + fn color (&self) -> ItemPalette { + self.color + } +} + pub trait HasTracks: Send + Sync { fn tracks (&self) -> &Vec; fn tracks_mut (&mut self) -> &mut Vec; @@ -134,33 +165,3 @@ impl ArrangerTracksApi for ArrangerTui { } } -#[derive(Debug)] pub struct ArrangerTrack { - /// Name of track - pub(crate) name: Arc>, - /// Preferred width of track column - pub(crate) width: usize, - /// Identifying color of track - pub(crate) color: ItemPalette, - /// MIDI player state - pub(crate) player: PhrasePlayerModel, -} -has_clock!(|self:ArrangerTrack|self.player.clock()); -has_player!(|self:ArrangerTrack|self.player); -impl ArrangerTrackApi for ArrangerTrack { - /// Name of track - fn name (&self) -> &Arc> { - &self.name - } - /// Preferred width of track column - fn width (&self) -> usize { - self.width - } - /// Preferred width of track column - fn width_mut (&mut self) -> &mut usize { - &mut self.width - } - /// Identifying color of track - fn color (&self) -> ItemPalette { - self.color - } -} From 15751ea1379114b787b0717c9520fcd932326e20 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 21 Dec 2024 00:08:09 +0100 Subject: [PATCH 046/905] reduce numer of time modules --- crates/tek/src/core.rs | 7 +- crates/tek/src/time.rs | 6 +- crates/tek/src/time/bpm.rs | 5 - crates/tek/src/time/moment.rs | 112 +++++++++++++++++++++++ crates/tek/src/time/{ppq.rs => pulse.rs} | 29 ++++++ crates/tek/src/time/quant.rs | 25 ----- crates/tek/src/time/sr.rs | 12 +++ crates/tek/src/time/timebase.rs | 112 ----------------------- crates/tek/src/time/unit.rs | 15 ++- crates/tek/src/time/usec.rs | 13 --- 10 files changed, 166 insertions(+), 170 deletions(-) delete mode 100644 crates/tek/src/time/bpm.rs rename crates/tek/src/time/{ppq.rs => pulse.rs} (63%) delete mode 100644 crates/tek/src/time/quant.rs delete mode 100644 crates/tek/src/time/timebase.rs delete mode 100644 crates/tek/src/time/usec.rs diff --git a/crates/tek/src/core.rs b/crates/tek/src/core.rs index 0a469b98..6c4c98df 100644 --- a/crates/tek/src/core.rs +++ b/crates/tek/src/core.rs @@ -24,15 +24,18 @@ pub use self::{ } pub trait Gettable { + /// Returns current value fn get (&self) -> T; } pub trait Mutable: Gettable { - fn set (&mut self, value: T); + /// Sets new value, returns old + fn set (&mut self, value: T) -> T; } pub trait InteriorMutable: Gettable { - fn set (&self, value: T); + /// Sets new value, returns old + fn set (&self, value: T) -> T; } /// Standard result type. diff --git a/crates/tek/src/time.rs b/crates/tek/src/time.rs index 4c37e243..0ef9a5db 100644 --- a/crates/tek/src/time.rs +++ b/crates/tek/src/time.rs @@ -1,13 +1,9 @@ -pub(crate) mod bpm; pub(crate) use bpm::*; pub(crate) mod clock; pub(crate) use clock::*; pub(crate) mod moment; pub(crate) use moment::*; pub(crate) mod perf; pub(crate) use perf::*; -pub(crate) mod ppq; pub(crate) use ppq::*; -pub(crate) mod quant; pub(crate) use quant::*; +pub(crate) mod pulse; pub(crate) use pulse::*; pub(crate) mod sr; pub(crate) use sr::*; -pub(crate) mod timebase; pub(crate) use timebase::*; pub(crate) mod unit; pub(crate) use unit::*; -pub(crate) mod usec; pub(crate) use usec::*; /// (pulses, name), assuming 96 PPQ pub const NOTE_DURATIONS: [(usize, &str);26] = [ diff --git a/crates/tek/src/time/bpm.rs b/crates/tek/src/time/bpm.rs deleted file mode 100644 index 30fffb9d..00000000 --- a/crates/tek/src/time/bpm.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::*; - -/// Tempo in beats per minute -#[derive(Debug, Default)] pub struct BeatsPerMinute(AtomicF64); -impl_time_unit!(BeatsPerMinute); diff --git a/crates/tek/src/time/moment.rs b/crates/tek/src/time/moment.rs index aafaef6a..79e0a68a 100644 --- a/crates/tek/src/time/moment.rs +++ b/crates/tek/src/time/moment.rs @@ -20,6 +20,7 @@ pub struct Moment { /// Current time in MIDI pulses pub pulse: Pulse, } + impl Moment { pub fn zero (timebase: &Arc) -> Self { Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } @@ -67,3 +68,114 @@ impl Moment { self.timebase.format_beats_1(self.pulse.get()) } } + +/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) +#[derive(Debug, Clone)] +pub struct Timebase { + /// Audio samples per second + pub sr: SampleRate, + /// MIDI beats per minute + pub bpm: BeatsPerMinute, + /// MIDI ticks per beat + pub ppq: PulsesPerQuaver, +} + +impl Timebase { + /// Specify sample rate, BPM and PPQ + pub fn new ( + s: impl Into, + b: impl Into, + p: impl Into + ) -> Self { + Self { sr: s.into(), bpm: b.into(), ppq: p.into() } + } + /// Iterate over ticks between start and end. + #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator { + TicksIterator { spp: self.samples_per_pulse(), sample: start, start, end } + } + /// Return the duration fo a beat in microseconds + #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } + /// Return the number of beats in a second + #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } + /// Return the number of microseconds corresponding to a note of the given duration + #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { + 4.0 * self.usec_per_beat() * num / den + } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } + /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) + #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } + /// Return number of pulses in a second (BPM-dependent) + #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } + /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) + #[inline] pub fn pulses_per_sample (&self) -> f64 { + self.usec_per_pulse() / self.sr.usec_per_sample() + } + /// Return number of samples in a pulse (SR- and BPM-dependent) + #[inline] pub fn samples_per_pulse (&self) -> f64 { + self.sr.get() / self.pulses_per_second() + } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { + self.pulses_per_sample() * p + } + /// Convert a number of samples to a pulse number (SR- and BPM-dependent) + #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { + s / self.pulses_per_sample() + } + /// Return the number of samples corresponding to a note of the given duration + #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { + self.usec_to_sample(self.note_to_usec(note)) + } + /// Return the number of samples corresponding to the given number of microseconds + #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { + usec * self.sr.get() / 1000f64 + } + /// Return the quantized position of a moment in time given a step + #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { + let step = self.note_to_usec(step); + (time / step, time % step) + } + /// Quantize a collection of events + #[inline] pub fn quantize_into + Sized, T> ( + &self, step: (f64, f64), events: E + ) -> Vec<(f64, f64)> { + events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 0 + #[inline] pub fn format_beats_0 (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + format!("{}.{}.{pulses:02}", beats / 4, beats % 4) + } + /// Format a number of pulses into Beat.Bar starting from 0 + #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4, beats % 4) + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1 (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + format!("{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4 + 1, beats % 4 + 1) + } +} + +impl Default for Timebase { + fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) } +} diff --git a/crates/tek/src/time/ppq.rs b/crates/tek/src/time/pulse.rs similarity index 63% rename from crates/tek/src/time/ppq.rs rename to crates/tek/src/time/pulse.rs index 964b36eb..52096580 100644 --- a/crates/tek/src/time/ppq.rs +++ b/crates/tek/src/time/pulse.rs @@ -1,6 +1,7 @@ use crate::*; pub const DEFAULT_PPQ: f64 = 96.0; + /// FIXME: remove this and use PPQ from timebase everywhere: pub const PPQ: usize = 96; @@ -12,6 +13,34 @@ impl_time_unit!(PulsesPerQuaver); #[derive(Debug, Default)] pub struct Pulse(AtomicF64); impl_time_unit!(Pulse); +/// Tempo in beats per minute +#[derive(Debug, Default)] pub struct BeatsPerMinute(AtomicF64); +impl_time_unit!(BeatsPerMinute); + +/// Quantization setting for launching clips +#[derive(Debug, Default)] pub struct LaunchSync(AtomicF64); +impl_time_unit!(LaunchSync); +impl LaunchSync { + pub fn next (&self) -> f64 { + next_note_length(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + prev_note_length(self.get() as usize) as f64 + } +} + +/// Quantization setting for notes +#[derive(Debug, Default)] pub struct Quantize(AtomicF64); +impl_time_unit!(Quantize); +impl Quantize { + pub fn next (&self) -> f64 { + next_note_length(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + prev_note_length(self.get() as usize) as f64 + } +} + /// Iterator that emits subsequent ticks within a range. pub struct TicksIterator { pub spp: f64, diff --git a/crates/tek/src/time/quant.rs b/crates/tek/src/time/quant.rs deleted file mode 100644 index dc05e271..00000000 --- a/crates/tek/src/time/quant.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::*; - -/// Quantization setting for launching clips -#[derive(Debug, Default)] pub struct LaunchSync(AtomicF64); -impl_time_unit!(LaunchSync); -impl LaunchSync { - pub fn next (&self) -> f64 { - next_note_length(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - prev_note_length(self.get() as usize) as f64 - } -} - -/// Quantization setting for notes -#[derive(Debug, Default)] pub struct Quantize(AtomicF64); -impl_time_unit!(Quantize); -impl Quantize { - pub fn next (&self) -> f64 { - next_note_length(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - prev_note_length(self.get() as usize) as f64 - } -} diff --git a/crates/tek/src/time/sr.rs b/crates/tek/src/time/sr.rs index 1d3edd75..bdaf5190 100644 --- a/crates/tek/src/time/sr.rs +++ b/crates/tek/src/time/sr.rs @@ -1,5 +1,17 @@ use crate::*; +/// Timestamp in microseconds +#[derive(Debug, Default)] pub struct Microsecond(AtomicF64); +impl_time_unit!(Microsecond); +impl Microsecond { + #[inline] pub fn format_msu (&self) -> String { + let usecs = self.get() as usize; + let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); + let (minutes, seconds) = (seconds / 60, seconds % 60); + format!("{minutes}:{seconds:02}:{msecs:03}") + } +} + /// Audio sample rate in Hz (samples per second) #[derive(Debug, Default)] pub struct SampleRate(AtomicF64); impl_time_unit!(SampleRate); diff --git a/crates/tek/src/time/timebase.rs b/crates/tek/src/time/timebase.rs deleted file mode 100644 index cc898b92..00000000 --- a/crates/tek/src/time/timebase.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::*; - -/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) -#[derive(Debug, Clone)] -pub struct Timebase { - /// Audio samples per second - pub sr: SampleRate, - /// MIDI beats per minute - pub bpm: BeatsPerMinute, - /// MIDI ticks per beat - pub ppq: PulsesPerQuaver, -} - -impl Timebase { - /// Specify sample rate, BPM and PPQ - pub fn new ( - s: impl Into, - b: impl Into, - p: impl Into - ) -> Self { - Self { sr: s.into(), bpm: b.into(), ppq: p.into() } - } - /// Iterate over ticks between start and end. - #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator { - TicksIterator { spp: self.samples_per_pulse(), sample: start, start, end } - } - /// Return the duration fo a beat in microseconds - #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } - /// Return the number of beats in a second - #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } - /// Return the number of microseconds corresponding to a note of the given duration - #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { - 4.0 * self.usec_per_beat() * num / den - } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } - /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) - #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } - /// Return number of pulses in a second (BPM-dependent) - #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } - /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) - #[inline] pub fn pulses_per_sample (&self) -> f64 { - self.usec_per_pulse() / self.sr.usec_per_sample() - } - /// Return number of samples in a pulse (SR- and BPM-dependent) - #[inline] pub fn samples_per_pulse (&self) -> f64 { - self.sr.get() / self.pulses_per_second() - } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { - self.pulses_per_sample() * p - } - /// Convert a number of samples to a pulse number (SR- and BPM-dependent) - #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { - s / self.pulses_per_sample() - } - /// Return the number of samples corresponding to a note of the given duration - #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { - self.usec_to_sample(self.note_to_usec(note)) - } - /// Return the number of samples corresponding to the given number of microseconds - #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { - usec * self.sr.get() / 1000f64 - } - /// Return the quantized position of a moment in time given a step - #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { - let step = self.note_to_usec(step); - (time / step, time % step) - } - /// Quantize a collection of events - #[inline] pub fn quantize_into + Sized, T> ( - &self, step: (f64, f64), events: E - ) -> Vec<(f64, f64)> { - events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 0 - #[inline] pub fn format_beats_0 (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - format!("{}.{}.{pulses:02}", beats / 4, beats % 4) - } - /// Format a number of pulses into Beat.Bar starting from 0 - #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4, beats % 4) - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1 (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - format!("{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4 + 1, beats % 4 + 1) - } -} - -impl Default for Timebase { - fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) } -} diff --git a/crates/tek/src/time/unit.rs b/crates/tek/src/time/unit.rs index 940cd65b..0abdaaa3 100644 --- a/crates/tek/src/time/unit.rs +++ b/crates/tek/src/time/unit.rs @@ -1,3 +1,4 @@ +use crate::*; /// A unit of time, represented as an atomic 64-bit float. /// @@ -5,14 +6,9 @@ /// every integer between 1 and 2^53 can be represented exactly. /// This should mean that, even at 192kHz sampling rate, over 1 year of audio /// can be clocked in microseconds with f64 without losing precision. -pub trait TimeUnit { - /// Returns current value - fn get (&self) -> f64; - /// Sets new value, returns old - fn set (&self, value: f64) -> f64; -} +pub trait TimeUnit: InteriorMutable {} -/// Implement arithmetic for a unit of time +/// Implement an arithmetic operation for a unit of time #[macro_export] macro_rules! impl_op { ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { impl $Op for $T { @@ -36,14 +32,17 @@ pub trait TimeUnit { /// Define and implement a unit of time #[macro_export] macro_rules! impl_time_unit { ($T:ident) => { - impl TimeUnit for $T { + impl Gettable for $T { fn get (&self) -> f64 { self.0.load(Ordering::Relaxed) } + } + impl InteriorMutable for $T { fn set (&self, value: f64) -> f64 { let old = self.get(); self.0.store(value, Ordering::Relaxed); old } } + impl TimeUnit for $T {} impl_op!($T, Add, add, |a, b|{a + b}); impl_op!($T, Sub, sub, |a, b|{a - b}); impl_op!($T, Mul, mul, |a, b|{a * b}); diff --git a/crates/tek/src/time/usec.rs b/crates/tek/src/time/usec.rs deleted file mode 100644 index 8f768861..00000000 --- a/crates/tek/src/time/usec.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::*; - -/// Timestamp in microseconds -#[derive(Debug, Default)] pub struct Microsecond(AtomicF64); -impl_time_unit!(Microsecond); -impl Microsecond { - #[inline] pub fn format_msu (&self) -> String { - let usecs = self.get() as usize; - let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); - let (minutes, seconds) = (seconds / 60, seconds % 60); - format!("{minutes}:{seconds:02}:{msecs:03}") - } -} From 958885686e29d60e9eb6eb2c7d73ed9ca5e06e4a Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 21 Dec 2024 00:15:36 +0100 Subject: [PATCH 047/905] special handling of borders where w/h is 1 --- crates/tek/src/core/color.rs | 2 -- crates/tek/src/tui/app_arranger.rs | 10 +++---- crates/tek/src/tui/arranger_mode_v.rs | 2 +- crates/tek/src/tui/tui_border.rs | 41 ++++++++++++++++----------- 4 files changed, 29 insertions(+), 26 deletions(-) diff --git a/crates/tek/src/core/color.rs b/crates/tek/src/core/color.rs index 93f086b6..519cb20a 100644 --- a/crates/tek/src/core/color.rs +++ b/crates/tek/src/core/color.rs @@ -24,9 +24,7 @@ pub struct ItemPalette { pub darker: ItemColor, pub darkest: ItemColor, } -/// Adds TUI RGB representation to an OKHSL value. from!(|okhsl: Okhsl|ItemColor = Self { okhsl, rgb: okhsl_to_rgb(okhsl) }); -/// Adds OKHSL representation to a TUI RGB value. from!(|rgb: Color|ItemColor = Self { rgb, okhsl: rgb_to_okhsl(rgb) }); // A single color within item theme parameters, in OKHSL and RGB representations. impl ItemColor { diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 470a1391..70524722 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -64,12 +64,10 @@ render!(|self: ArrangerTui|{ let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); let transport = TransportView::from((self, None, true)); let with_transport = |x|col!([row!(![&play, &transport]), &x]); - let with_pool = |x|Split::left(false, if self.show_pool { - self.splits[1] - } else { - 0 - }, PhraseListView(&self.phrases), x); - with_transport(with_pool(col!([ + let with_pool = |x|Split::left(false, + if self.show_pool { self.splits[1] } else { 0 }, + PhraseListView(&self.phrases), x); + with_pool(with_transport(col!([ &self.size, Fill::w(Fixed::h(20, arranger())), Fill::w(Fixed::h(25, &self.editor)), diff --git a/crates/tek/src/tui/arranger_mode_v.rs b/crates/tek/src/tui/arranger_mode_v.rs index f8d54086..0909b5b3 100644 --- a/crates/tek/src/tui/arranger_mode_v.rs +++ b/crates/tek/src/tui/arranger_mode_v.rs @@ -188,7 +188,7 @@ render!(|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{ area }, }; - let bg = TuiTheme::border_bg(); + let bg = Color::Rgb(0, 255, 0); if let Some([x, y, width, height]) = track_area { to.fill_fg([x, y, 1, height], bg); to.fill_fg([x + width, y, 1, height], bg); diff --git a/crates/tek/src/tui/tui_border.rs b/crates/tek/src/tui/tui_border.rs index b7860f6d..14abf9f0 100644 --- a/crates/tek/src/tui/tui_border.rs +++ b/crates/tek/src/tui/tui_border.rs @@ -44,6 +44,12 @@ pub trait BorderStyle: Send + Sync + Copy { const S: &'static str = ""; const SW: &'static str = ""; const W: &'static str = ""; + + const N0: &'static str = ""; + const S0: &'static str = ""; + const W0: &'static str = ""; + const E0: &'static str = ""; + fn n (&self) -> &str { Self::N } fn s (&self) -> &str { Self::S } fn e (&self) -> &str { Self::E } @@ -67,30 +73,26 @@ pub trait BorderStyle: Send + Sync + Copy { let style = style.or_else(||self.style_horizontal()); let [x, x2, y, y2] = area.lrtb(); for x in x..x2.saturating_sub(1) { - self.draw_north(to, x, y, style); - self.draw_south(to, x, y2.saturating_sub(1), style); + to.blit(&Self::N, x, y, style); + to.blit(&Self::S, x, y2.saturating_sub(1), style) } Ok(area) } - #[inline] fn draw_north ( - &self, to: &mut TuiOutput, x: u16, y: u16, style: Option