diff --git a/Justfile b/Justfile index 3654e2fb..6c5ebfa0 100644 --- a/Justfile +++ b/Justfile @@ -59,7 +59,7 @@ arranger-release-ext: groovebox: reset - cargo run --bin tek_groovebox + cargo run --bin tek_groovebox -- -b 174 groovebox-ext: reset cargo run --bin tek_groovebox -- -n tek \ @@ -79,6 +79,15 @@ groovebox-release-ext: -r "Komplete Audio 6 Pro:capture_AUX1" \ -L "Komplete Audio 6 Pro:playback_AUX1" \ -R "Komplete Audio 6 Pro:playback_AUX2" +groovebox-release-ext-browser: + reset + cargo run --release --bin tek_groovebox -- -n tek \ + -b 112 \ + -i "Midi-Bridge:nanoKEY Studio 1:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -l "Firefox:output_FL" \ + -r "Firefox:output_FR" \ + -L "Komplete Audio 6 Pro:playback_AUX1" \ + -R "Komplete Audio 6 Pro:playback_AUX2" sequencer: reset diff --git a/bin/cli_groovebox.rs b/bin/cli_groovebox.rs index f398870e..472916e3 100644 --- a/bin/cli_groovebox.rs +++ b/bin/cli_groovebox.rs @@ -15,6 +15,9 @@ pub struct GrooveboxCli { /// Whether to attempt to become transport master #[arg(short='s', long, default_value_t = true)] sync_follow: bool, + /// Default BPM + #[arg(short='b', long, default_value = None)] + bpm: Option, /// MIDI outs to connect to MIDI input #[arg(short='i', long)] midi_from: Vec, @@ -46,6 +49,9 @@ impl GrooveboxCli { &[&self.l_from.as_slice(), &self.r_from.as_slice()], &[&self.l_to.as_slice(), &self.r_to.as_slice()], )?; + if let Some(bpm) = self.bpm { + app.clock().timebase.bpm.set(bpm); + } if self.sync_lead { jack.read().unwrap().client().register_timebase_callback(false, |mut state|{ app.clock().playhead.update_from_sample(state.position.frame() as f64); diff --git a/src/clock/clock_tui.rs b/src/clock/clock_tui.rs index 6d866858..2584c064 100644 --- a/src/clock/clock_tui.rs +++ b/src/clock/clock_tui.rs @@ -38,15 +38,6 @@ render!(Tui: (self: TransportView<'a>) => Outer( OutputStats::new(self.compact, self.clock), ))); -struct Field<'a>(ItemPalette, &'a str, &'a str); -render!(Tui: (self: Field<'a>) => row!( - Tui::bg(self.0.darkest.rgb, Tui::fg(self.0.darker.rgb, "▐")), - Tui::bg(self.0.darker.rgb, Tui::fg(self.0.lighter.rgb, - Tui::bold(true, format!("{}", self.1)))), - Tui::bg(self.0.darkest.rgb, Tui::fg(self.0.darker.rgb, "▌")), - Tui::bg(self.0.darkest.rgb, Tui::fg(self.0.lightest.rgb, - Tui::bold(true, format!("{} ", self.2)))))); - pub struct PlayPause { pub compact: bool, pub playing: bool } render!(Tui: (self: PlayPause) => Tui::bg( if self.playing{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}, diff --git a/src/field.rs b/src/field.rs new file mode 100644 index 00000000..19dad2a3 --- /dev/null +++ b/src/field.rs @@ -0,0 +1,22 @@ +use crate::*; + +pub struct Field(pub ItemPalette, pub T, pub U) + where T: AsRef + Send + Sync, U: AsRef + Send + Sync; + +impl Content for Field + where T: AsRef + Send + Sync, U: AsRef + Send + Sync +{ + fn content (&self) -> impl Content { + row!( + Tui::bg(self.0.darkest.rgb, Tui::fg(self.0.darker.rgb, "▐")), + + Tui::bg(self.0.darker.rgb, Tui::fg(self.0.lighter.rgb, + Tui::bold(true, format!("{}", self.1.as_ref())))), + + Tui::bg(self.0.darkest.rgb, Tui::fg(self.0.darker.rgb, "▌")), + + Tui::bg(self.0.darkest.rgb, Tui::fg(self.0.lightest.rgb, + format!("{} ", self.2.as_ref()))) + ) + } +} diff --git a/src/groovebox.rs b/src/groovebox.rs index cc19d5ab..54a9b237 100644 --- a/src/groovebox.rs +++ b/src/groovebox.rs @@ -131,20 +131,16 @@ render!(Tui: (self: Groovebox) => { })), ), Bsp::n( - Bsp::e( - Fixed::y(1, SamplerStatus(&self.sampler, note_pt)), - MidiEditStatus(&self.editor), + lay!( + Align::w(Fixed::y(1, SamplerStatus(&self.sampler, note_pt))), + Align::x(Fixed::y(1, MidiEditStatus(&self.editor))), ), Bsp::w( Fixed::x(pool_w, Align::e(Fill::y(PoolView(&self.pool)))), Fill::xy(Bsp::e( - Fixed::x(sampler_w, sampler), - Bsp::s( - selector, - &self.editor, - ), - ), - ) + Fixed::x(sampler_w, Push::y(3, sampler)), + Bsp::s(selector, &self.editor), + )), ), ) ) diff --git a/src/lib.rs b/src/lib.rs index 063e14fb..536a212e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,7 @@ pub mod border; pub use self::border::*; pub mod clock; pub use self::clock::*; pub mod color; pub use self::color::*; pub mod command; pub use self::command::*; +pub mod field; pub use self::field::*; pub mod file; pub use self::file::*; pub mod focus; pub use self::focus::*; pub mod groovebox; pub use self::groovebox::*; diff --git a/src/midi.rs b/src/midi.rs index 7eed3c1a..c433e6cd 100644 --- a/src/midi.rs +++ b/src/midi.rs @@ -13,6 +13,7 @@ pub(crate) mod midi_point; pub(crate) use midi_point::*; pub(crate) mod midi_view; pub(crate) use midi_view::*; pub(crate) mod midi_editor; pub(crate) use midi_editor::*; +pub(crate) mod midi_status; pub(crate) use midi_status::*; /// Add "all notes off" to the start of a buffer. pub fn all_notes_off (output: &mut [Vec>]) { diff --git a/src/midi/midi_status.rs b/src/midi/midi_status.rs new file mode 100644 index 00000000..ae0af245 --- /dev/null +++ b/src/midi/midi_status.rs @@ -0,0 +1,25 @@ +use crate::*; + +pub struct MidiEditStatus<'a>(pub &'a MidiEditor); +render!(Tui: (self: MidiEditStatus<'a>) => { + 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 { + (ItemPalette::from(TuiTheme::g(64)), String::new(), 0, false) + }; + let time_point = self.0.time_point(); + let time_start = self.0.time_start(); + let time_end = self.0.time_end(); + let time_axis = self.0.time_axis().get(); + let time_zoom = self.0.time_zoom().get(); + let time_lock = if self.0.time_lock().get() { "[lock]" } else { " " }; + let time_field = Field(color, "Time", format!("{length}/{time_zoom}+{time_point} {time_lock}")); + + Tui::bg(color.darkest.rgb, Fill::x(Tui::fg(color.lightest.rgb, Bsp::e( + time_field, + Field(color, "Note", format!("{} ({}) {} | {}-{} ({})", + self.0.note_point(), Note::pitch_to_name(self.0.note_point()), self.0.note_len(), + Note::pitch_to_name(self.0.note_lo().get()), Note::pitch_to_name(self.0.note_hi()), + self.0.note_axis().get())) + )))) +}); diff --git a/src/pool/phrase_selector.rs b/src/pool/phrase_selector.rs index 1e18c9b2..a4aa5601 100644 --- a/src/pool/phrase_selector.rs +++ b/src/pool/phrase_selector.rs @@ -7,14 +7,8 @@ pub struct PhraseSelector { pub(crate) time: String, } -// TODO: Display phrases always in order of appearance -render!(Tui: (self: PhraseSelector) => Fixed::xy(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())]), - Tui::bg(self.color.dark.rgb, &self.time), - )), -))); +render!(Tui: (self: PhraseSelector) => + Field(self.color, self.title, format!("{} {}", self.time, self.name))); impl PhraseSelector { @@ -31,7 +25,7 @@ impl PhraseSelector { } else { String::from(" ") }; - Self { title: "Now:|", time, name, color, } + Self { title: "Now", time, name, color, } } // beats until switchover @@ -59,7 +53,7 @@ impl PhraseSelector { } else { (" ".into(), " ".into(), TuiTheme::g(64).into()) }; - Self { title: " Next|", time, name, color, } + Self { title: "Next", time, name, color, } } } diff --git a/src/sampler.rs b/src/sampler.rs index 5e6e23cd..ef02bc21 100644 --- a/src/sampler.rs +++ b/src/sampler.rs @@ -181,6 +181,9 @@ impl Sampler { voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); } }, + MidiMessage::Controller { controller, value } => { + // TODO + } _ => {} } } diff --git a/src/status.rs b/src/status.rs index c8b379ef..c3559e88 100644 --- a/src/status.rs +++ b/src/status.rs @@ -1,32 +1,5 @@ use crate::*; -pub struct MidiEditStatus<'a>(pub &'a MidiEditor); -render!(Tui: (self:MidiEditStatus<'a>) => { - 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 { - (ItemPalette::from(TuiTheme::g(64)), String::new(), 0, false) - }; - let field = move|x, y|row!( - Tui::fg_bg(color.lighter.rgb, color.darker.rgb, Tui::bold(true, x)), - Tui::fg_bg(color.light.rgb, color.darker.rgb, Tui::bold(true, "│")), - Tui::fg_bg(color.lightest.rgb, color.dark.rgb, y), - ); - let bg = color.darkest.rgb; - let fg = color.lightest.rgb; - Tui::bg(bg, Fill::x(Tui::fg(fg, row!( - field(" Time", format!("{}/{}-{} ({}*{}) {}", - self.0.time_point(), self.0.time_start().get(), self.0.time_end(), - self.0.time_axis().get(), self.0.time_zoom().get(), - if self.0.time_lock().get() { "[lock]" } else { " " })), - " ", - field(" Note", format!("{} ({}) {} | {}-{} ({})", - self.0.note_point(), Note::pitch_to_name(self.0.note_point()), self.0.note_len(), - Note::pitch_to_name(self.0.note_lo().get()), Note::pitch_to_name(self.0.note_hi()), - self.0.note_axis().get())) - )))) -}); - /// Status bar for sequencer app #[derive(Clone)] pub struct SequencerStatus {