mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
refactor groovebox into modules + accidental code golf
This commit is contained in:
parent
c2e3f73044
commit
005bb5fde8
4 changed files with 217 additions and 175 deletions
179
src/groovebox.rs
179
src/groovebox.rs
|
|
@ -6,6 +6,10 @@ use GrooveboxCommand as Cmd;
|
||||||
use MidiEditCommand::*;
|
use MidiEditCommand::*;
|
||||||
use PhrasePoolCommand::*;
|
use PhrasePoolCommand::*;
|
||||||
|
|
||||||
|
mod groovebox_audio; pub use self::groovebox_audio::*;
|
||||||
|
mod groovebox_command; pub use self::groovebox_command::*;
|
||||||
|
mod groovebox_tui; pub use self::groovebox_tui::*;
|
||||||
|
|
||||||
pub struct Groovebox {
|
pub struct Groovebox {
|
||||||
_jack: Arc<RwLock<JackConnection>>,
|
_jack: Arc<RwLock<JackConnection>>,
|
||||||
|
|
||||||
|
|
@ -55,178 +59,3 @@ impl Groovebox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
has_clock!(|self: Groovebox|self.player.clock());
|
has_clock!(|self: Groovebox|self.player.clock());
|
||||||
audio!(|self: Groovebox, client, scope|{
|
|
||||||
let t0 = self.perf.get_t0();
|
|
||||||
if Control::Quit == ClockAudio(&mut self.player).process(client, scope) {
|
|
||||||
return Control::Quit
|
|
||||||
}
|
|
||||||
if Control::Quit == PlayerAudio(
|
|
||||||
&mut self.player, &mut self.note_buf, &mut self.midi_buf
|
|
||||||
).process(client, scope) {
|
|
||||||
return Control::Quit
|
|
||||||
}
|
|
||||||
if Control::Quit == SamplerAudio(&mut self.sampler).process(client, scope) {
|
|
||||||
return Control::Quit
|
|
||||||
}
|
|
||||||
// TODO move these to editor and sampler:
|
|
||||||
for RawMidi { time, bytes } in self.player.midi_ins[0].iter(scope) {
|
|
||||||
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
|
|
||||||
match message {
|
|
||||||
MidiMessage::NoteOn { ref key, .. } => {
|
|
||||||
self.editor.set_note_point(key.as_int() as usize);
|
|
||||||
},
|
|
||||||
MidiMessage::Controller { controller, value } => {
|
|
||||||
if let Some(sample) = &self.sampler.mapped[self.editor.note_point()] {
|
|
||||||
sample.write().unwrap().handle_cc(controller, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.perf.update(t0, scope);
|
|
||||||
Control::Continue
|
|
||||||
});
|
|
||||||
|
|
||||||
render!(Tui: (self: Groovebox) => self.size.of(Bsp::s(
|
|
||||||
self.toolbar_view(),
|
|
||||||
Bsp::s(
|
|
||||||
self.selector_view(),
|
|
||||||
Bsp::n(self.sample_view(),
|
|
||||||
Bsp::n(self.status_view(),
|
|
||||||
Bsp::w(
|
|
||||||
self.pool_view(),
|
|
||||||
Fill::xy(Bsp::e(
|
|
||||||
self.sampler_view(),
|
|
||||||
&self.editor
|
|
||||||
)))))))));
|
|
||||||
|
|
||||||
impl Groovebox {
|
|
||||||
fn toolbar_view (&self) -> impl Content<Tui> + use<'_> {
|
|
||||||
Fill::x(Fixed::y(2, lay!(
|
|
||||||
Align::w(Meter("L/", self.sampler.input_meter[0])),
|
|
||||||
Align::e(Meter("R/", self.sampler.input_meter[1])),
|
|
||||||
Align::x(Tui::bg(TuiTheme::g(32), TransportView::new(true, &self.player.clock))),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
fn status_view (&self) -> impl Content<Tui> + use<'_> {
|
|
||||||
let note_pt = self.editor.note_point();
|
|
||||||
lay!(
|
|
||||||
Align::w(Fixed::y(1, SamplerStatus(&self.sampler, note_pt))),
|
|
||||||
Align::x(Fixed::y(1, MidiEditStatus(&self.editor))),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fn sampler_view (&self) -> impl Content<Tui> + use<'_> {
|
|
||||||
let sampler_w = if !self.compact { 11 } else { 4 };
|
|
||||||
let sampler_y = if self.compact { 1 } else { 0 };
|
|
||||||
Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(
|
|
||||||
SampleList::new(self.compact, &self.sampler, &self.editor))))
|
|
||||||
}
|
|
||||||
fn sample_view (&self) -> impl Content<Tui> + use<'_> {
|
|
||||||
let note_pt = self.editor.note_point();
|
|
||||||
let sample_h = if self.compact { 0 } else { 5 };
|
|
||||||
Max::y(sample_h, Fill::xy(
|
|
||||||
SampleViewer::from_sampler(&self.sampler, note_pt)))
|
|
||||||
}
|
|
||||||
fn pool_view (&self) -> impl Content<Tui> + use<'_> {
|
|
||||||
let w = self.size.w();
|
|
||||||
let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
|
||||||
Fixed::x(if self.compact { 5 } else { pool_w },
|
|
||||||
PoolView(self.compact, &self.pool))
|
|
||||||
}
|
|
||||||
fn selector_view (&self) -> impl Content<Tui> + use<'_> {
|
|
||||||
lay!(
|
|
||||||
Align::w(MidiEditClip(&self.editor)),
|
|
||||||
Align::e(Bsp::e(
|
|
||||||
ClipSelected::play_phrase(&self.player),
|
|
||||||
ClipSelected::next_phrase(&self.player)
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum GrooveboxCommand {
|
|
||||||
Compact(bool),
|
|
||||||
History(isize),
|
|
||||||
Clock(ClockCommand),
|
|
||||||
Pool(PoolCommand),
|
|
||||||
Editor(MidiEditCommand),
|
|
||||||
Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
|
||||||
Sampler(SamplerCommand),
|
|
||||||
}
|
|
||||||
|
|
||||||
handle!(<Tui>|self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input.event()));
|
|
||||||
keymap!(KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand {
|
|
||||||
// Tab: Toggle compact mode
|
|
||||||
key(Tab) => Cmd::Compact(!state.compact),
|
|
||||||
// q: Enqueue currently edited phrase
|
|
||||||
key(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())),
|
|
||||||
// 0: Enqueue phrase 0 (stop all)
|
|
||||||
key(Char('0')) => Cmd::Enqueue(Some(state.pool.phrases()[0].clone())),
|
|
||||||
// TODO: k: toggle on-screen keyboard
|
|
||||||
ctrl(key(Char('k'))) => todo!("keyboard"),
|
|
||||||
// Transport: Play from start or rewind to start
|
|
||||||
ctrl(key(Char(' '))) => Cmd::Clock(
|
|
||||||
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
|
|
||||||
),
|
|
||||||
// Shift-R: toggle recording
|
|
||||||
shift(key(Char('R'))) => Cmd::Sampler(if state.sampler.recording.is_some() {
|
|
||||||
SamplerCommand::RecordFinish
|
|
||||||
} else {
|
|
||||||
SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8))
|
|
||||||
}),
|
|
||||||
// Shift-Del: delete sample
|
|
||||||
shift(key(Delete)) => Cmd::Sampler(
|
|
||||||
SamplerCommand::SetSample(u7::from(state.editor.note_point() as u8), None)
|
|
||||||
),
|
|
||||||
// e: Toggle between editing currently playing or other phrase
|
|
||||||
shift(key(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.pool.phrase().clone();
|
|
||||||
Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
|
||||||
selected
|
|
||||||
} else {
|
|
||||||
playing.clone()
|
|
||||||
})))
|
|
||||||
} else {
|
|
||||||
return None
|
|
||||||
},
|
|
||||||
}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) {
|
|
||||||
Cmd::Editor(command)
|
|
||||||
} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
|
|
||||||
Cmd::Pool(command)
|
|
||||||
} else {
|
|
||||||
return None
|
|
||||||
});
|
|
||||||
|
|
||||||
command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
|
||||||
Self::Enqueue(phrase) => {
|
|
||||||
state.player.enqueue_next(phrase.as_ref());
|
|
||||||
None
|
|
||||||
},
|
|
||||||
Self::Pool(cmd) => match cmd {
|
|
||||||
// autoselect: automatically load selected phrase in editor
|
|
||||||
PoolCommand::Select(_) => {
|
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
|
||||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
|
||||||
undo
|
|
||||||
},
|
|
||||||
// update color in all places simultaneously
|
|
||||||
PoolCommand::Phrase(SetColor(index, _)) => {
|
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
|
||||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
|
||||||
undo
|
|
||||||
},
|
|
||||||
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
|
||||||
},
|
|
||||||
Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?,
|
|
||||||
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
|
||||||
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
|
||||||
Self::History(delta) => { todo!("undo/redo") },
|
|
||||||
Self::Compact(compact) => if state.compact != compact {
|
|
||||||
state.compact = compact;
|
|
||||||
Some(Self::Compact(!compact))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
||||||
36
src/groovebox/groovebox_audio.rs
Normal file
36
src/groovebox/groovebox_audio.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
use crate::*;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
|
audio!(|self: Groovebox, client, scope|{
|
||||||
|
let t0 = self.perf.get_t0();
|
||||||
|
if Control::Quit == ClockAudio(&mut self.player).process(client, scope) {
|
||||||
|
return Control::Quit
|
||||||
|
}
|
||||||
|
if Control::Quit == PlayerAudio(
|
||||||
|
&mut self.player, &mut self.note_buf, &mut self.midi_buf
|
||||||
|
).process(client, scope) {
|
||||||
|
return Control::Quit
|
||||||
|
}
|
||||||
|
if Control::Quit == SamplerAudio(&mut self.sampler).process(client, scope) {
|
||||||
|
return Control::Quit
|
||||||
|
}
|
||||||
|
// TODO move these to editor and sampler:
|
||||||
|
for RawMidi { time, bytes } in self.player.midi_ins[0].iter(scope) {
|
||||||
|
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
|
||||||
|
match message {
|
||||||
|
MidiMessage::NoteOn { ref key, .. } => {
|
||||||
|
self.editor.set_note_point(key.as_int() as usize);
|
||||||
|
},
|
||||||
|
MidiMessage::Controller { controller, value } => {
|
||||||
|
if let Some(sample) = &self.sampler.mapped[self.editor.note_point()] {
|
||||||
|
sample.write().unwrap().handle_cc(controller, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.perf.update(t0, scope);
|
||||||
|
Control::Continue
|
||||||
|
});
|
||||||
91
src/groovebox/groovebox_command.rs
Normal file
91
src/groovebox/groovebox_command.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
use crate::*;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use self::GrooveboxCommand as Cmd;
|
||||||
|
use ClockCommand::{Play, Pause};
|
||||||
|
|
||||||
|
pub enum GrooveboxCommand {
|
||||||
|
Compact(bool),
|
||||||
|
History(isize),
|
||||||
|
Clock(ClockCommand),
|
||||||
|
Pool(PoolCommand),
|
||||||
|
Editor(MidiEditCommand),
|
||||||
|
Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
||||||
|
Sampler(SamplerCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
||||||
|
Self::Enqueue(phrase) => {
|
||||||
|
state.player.enqueue_next(phrase.as_ref());
|
||||||
|
None
|
||||||
|
},
|
||||||
|
Self::Pool(cmd) => match cmd {
|
||||||
|
// autoselect: automatically load selected phrase in editor
|
||||||
|
PoolCommand::Select(_) => {
|
||||||
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
|
state.editor.set_phrase(Some(state.pool.phrase()));
|
||||||
|
undo
|
||||||
|
},
|
||||||
|
// update color in all places simultaneously
|
||||||
|
PoolCommand::Phrase(SetColor(index, _)) => {
|
||||||
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
|
state.editor.set_phrase(Some(state.pool.phrase()));
|
||||||
|
undo
|
||||||
|
},
|
||||||
|
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
||||||
|
},
|
||||||
|
Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?,
|
||||||
|
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
||||||
|
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
||||||
|
Self::History(delta) => { todo!("undo/redo") },
|
||||||
|
Self::Compact(compact) => if state.compact != compact {
|
||||||
|
state.compact = compact;
|
||||||
|
Some(Self::Compact(!compact))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
handle!(<Tui>|self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input.event()));
|
||||||
|
keymap!(KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand {
|
||||||
|
// Tab: Toggle compact mode
|
||||||
|
key(Tab) => Cmd::Compact(!state.compact),
|
||||||
|
// q: Enqueue currently edited phrase
|
||||||
|
key(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())),
|
||||||
|
// 0: Enqueue phrase 0 (stop all)
|
||||||
|
key(Char('0')) => Cmd::Enqueue(Some(state.pool.phrases()[0].clone())),
|
||||||
|
// TODO: k: toggle on-screen keyboard
|
||||||
|
ctrl(key(Char('k'))) => todo!("keyboard"),
|
||||||
|
// Transport: Play from start or rewind to start
|
||||||
|
ctrl(key(Char(' '))) => Cmd::Clock(
|
||||||
|
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
|
||||||
|
),
|
||||||
|
// Shift-R: toggle recording
|
||||||
|
shift(key(Char('R'))) => Cmd::Sampler(if state.sampler.recording.is_some() {
|
||||||
|
SamplerCommand::RecordFinish
|
||||||
|
} else {
|
||||||
|
SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8))
|
||||||
|
}),
|
||||||
|
// Shift-Del: delete sample
|
||||||
|
shift(key(Delete)) => Cmd::Sampler(
|
||||||
|
SamplerCommand::SetSample(u7::from(state.editor.note_point() as u8), None)
|
||||||
|
),
|
||||||
|
// e: Toggle between editing currently playing or other phrase
|
||||||
|
shift(key(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.pool.phrase().clone();
|
||||||
|
Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
||||||
|
selected
|
||||||
|
} else {
|
||||||
|
playing.clone()
|
||||||
|
})))
|
||||||
|
} else {
|
||||||
|
return None
|
||||||
|
},
|
||||||
|
}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) {
|
||||||
|
Cmd::Editor(command)
|
||||||
|
} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
|
||||||
|
Cmd::Pool(command)
|
||||||
|
} else {
|
||||||
|
return None
|
||||||
|
});
|
||||||
86
src/groovebox/groovebox_tui.rs
Normal file
86
src/groovebox/groovebox_tui.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
use crate::*;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/*
|
||||||
|
render!(Tui: (self: Groovebox) => self.size.of(
|
||||||
|
self.toolbar_view()
|
||||||
|
.south(self.selector_view())
|
||||||
|
.south(self.sample_view())
|
||||||
|
.north(self.status_view())
|
||||||
|
.north(self.pool_view())
|
||||||
|
.west(Fill::xy(self.sampler_view().east(self.editor)))));
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
render!(Tui: (self: Groovebox) => self.size.of(
|
||||||
|
self.editor
|
||||||
|
.east_of(self.sample_view())
|
||||||
|
.wrap(Fill::xy)
|
||||||
|
.west_of(self.pool_view())
|
||||||
|
.north_of(self.status_view())
|
||||||
|
.north_of(self.sample_view())
|
||||||
|
.south_of(self.selector_view())
|
||||||
|
.south_of(self.toolbar_view())
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
render!(Tui: (self: Groovebox) => self.size.of(
|
||||||
|
self.toolbar_view().north_of(
|
||||||
|
self.selector_view().north_of(
|
||||||
|
self.sample_view().south_of(
|
||||||
|
self.status_view().south_of(
|
||||||
|
self.pool_view().east_of(
|
||||||
|
Fill::xy(self.sampler_view().west_of(
|
||||||
|
self.editor)))))))));
|
||||||
|
*/
|
||||||
|
|
||||||
|
render!(Tui: (self: Groovebox) => self.size.of(
|
||||||
|
Bsp::s(self.toolbar_view(),
|
||||||
|
Bsp::s(self.selector_view(),
|
||||||
|
Bsp::n(self.sample_view(),
|
||||||
|
Bsp::n(self.status_view(),
|
||||||
|
Bsp::w(self.pool_view(), Fill::xy(Bsp::e(self.sampler_view(), &self.editor)))))))));
|
||||||
|
|
||||||
|
impl Groovebox {
|
||||||
|
fn toolbar_view (&self) -> impl Content<Tui> + use<'_> {
|
||||||
|
Fill::x(Fixed::y(2, lay!(
|
||||||
|
Align::w(Meter("L/", self.sampler.input_meter[0])),
|
||||||
|
Align::e(Meter("R/", self.sampler.input_meter[1])),
|
||||||
|
Align::x(Tui::bg(TuiTheme::g(32), TransportView::new(true, &self.player.clock))),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
fn status_view (&self) -> impl Content<Tui> + use<'_> {
|
||||||
|
let note_pt = self.editor.note_point();
|
||||||
|
lay!(
|
||||||
|
Align::w(Fixed::y(1, SamplerStatus(&self.sampler, note_pt))),
|
||||||
|
Align::x(Fixed::y(1, MidiEditStatus(&self.editor))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn sampler_view (&self) -> impl Content<Tui> + use<'_> {
|
||||||
|
let sampler_w = if !self.compact { 11 } else { 4 };
|
||||||
|
let sampler_y = if self.compact { 1 } else { 0 };
|
||||||
|
Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(
|
||||||
|
SampleList::new(self.compact, &self.sampler, &self.editor))))
|
||||||
|
}
|
||||||
|
fn sample_view (&self) -> impl Content<Tui> + use<'_> {
|
||||||
|
let note_pt = self.editor.note_point();
|
||||||
|
let sample_h = if self.compact { 0 } else { 5 };
|
||||||
|
Max::y(sample_h, Fill::xy(
|
||||||
|
SampleViewer::from_sampler(&self.sampler, note_pt)))
|
||||||
|
}
|
||||||
|
fn pool_view (&self) -> impl Content<Tui> + use<'_> {
|
||||||
|
let w = self.size.w();
|
||||||
|
let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||||
|
Fixed::x(if self.compact { 5 } else { pool_w },
|
||||||
|
PoolView(self.compact, &self.pool))
|
||||||
|
}
|
||||||
|
fn selector_view (&self) -> impl Content<Tui> + use<'_> {
|
||||||
|
lay!(
|
||||||
|
Align::w(MidiEditClip(&self.editor)),
|
||||||
|
Align::e(Bsp::e(
|
||||||
|
ClipSelected::play_phrase(&self.player),
|
||||||
|
ClipSelected::next_phrase(&self.player)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue