add handle! macro and enable groovebox

This commit is contained in:
🪞👃🪞 2024-12-17 01:57:22 +01:00
parent 5c630cc51b
commit a352141dde
13 changed files with 350 additions and 309 deletions

View file

@ -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

View file

@ -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"

View file

@ -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(())
}
}

View file

@ -17,6 +17,16 @@ pub trait Handle<E: Engine>: Send + Sync {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled>;
}
#[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<E: Engine, H: Handle<E>> Handle<E> for &mut H {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
(*self).handle(context)

View file

@ -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;

View file

@ -164,8 +164,7 @@ impl Content<Tui> for Track<Tui> {
}
}
impl Handle<Tui> for Mixer<Tui> {
fn handle (&mut self, engine: &TuiInput) -> Perhaps<bool> {
handle!(<Tui>|self:Mixer,engine|{
if let TuiEvent::Input(crossterm::event::Event::Key(event)) = engine.event() {
match event.code {
@ -211,10 +210,9 @@ impl Handle<Tui> for Mixer<Tui> {
}
Ok(None)
}
}
impl Handle<Tui> for Track<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
});
handle!(<Tui>|self:Track<Tui>,from|{
match from.event() {
//, NONE, "chain_cursor_up", "move cursor up", || {
key!(KeyCode::Up) => {
@ -247,5 +245,4 @@ impl Handle<Tui> for Track<Tui> {
},
_ => Ok(None)
}
}
}
});

View file

@ -83,8 +83,7 @@ fn draw_header <E> (state: &Plugin<E>, to: &mut TuiOutput, x: u16, y: u16, w: u1
Ok(Rect { x, y, width: w, height: 1 })
}
impl Handle<Tui> for Plugin<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
handle!(<Tui>|self:Plugin<tui>,from|{
match from.event() {
key!(KeyCode::Up) => {
self.selected = self.selected.saturating_sub(1);
@ -144,5 +143,5 @@ impl Handle<Tui> for Plugin<Tui> {
},
_ => Ok(None)
}
}
});
}

View file

@ -56,9 +56,94 @@ pub struct ArrangerTui {
pub perf: PerfModel,
}
impl Handle<Tui> for ArrangerTui {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
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!(<Tui>|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<PhraseListMode> {
&self.phrases.mode
}
fn phrase_index (&self) -> usize {
self.phrases.phrase.load(Ordering::Relaxed)
}
}
@ -320,97 +405,8 @@ impl TransportControl<ArrangerFocus> 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<PhraseListMode> {
&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)]

View file

@ -12,7 +12,7 @@ impl TryFrom<&Arc<RwLock<JackClient>>> 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!(<Tui>|self:GrooveboxTui,input|Ok(None));

View file

@ -221,8 +221,7 @@ fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
todo!();
}
impl Handle<Tui> for SamplerTui {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
handle!(<Tui>|self:SamplerTui,from|{
let cursor = &mut self.cursor;
let unmapped = &mut self.state.unmapped;
let mapped = &self.state.mapped;
@ -255,8 +254,7 @@ impl Handle<Tui> for SamplerTui {
}
}
Ok(Some(true))
}
}
});
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
let (mut subdirs, mut files) = std::fs::read_dir(dir)?

View file

@ -144,24 +144,11 @@ impl InputToCommand<Tui, SequencerTui> 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!(<Tui>|self:SequencerTui|&self.size);
has_clock!(|self:SequencerTui|&self.clock);
has_phrases!(|self:SequencerTui|self.phrases.phrases);
has_editor!(|self:SequencerTui|self.editor);
handle!(<Tui>|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!(<Tui>|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<Tui> for SequencerTui {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
SequencerCommand::execute_with_state(self, i)
}
}
/// Status bar for sequencer app
#[derive(Clone)]
pub struct SequencerStatusBar {

View file

@ -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!(<Tui>|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<Tui> for TransportTui {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
TransportCommand::execute_with_state(self, from)
}
}
pub trait TransportControl<T>: HasClock + {
fn transport_focused (&self) -> Option<TransportFocus>;
}

View file

@ -239,12 +239,13 @@ 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 let Some(cell) = buf.get_mut(x, note) {
if notes_on[note] {
cell.set_char('▂');
cell.set_style(style);
}
}
}
let time_end = time_start + zoom;
for time in time_start..time_end {