mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 12:16:42 +01:00
add command! and input_to_command! macros
This commit is contained in:
parent
93413ae303
commit
fdafd15a01
4 changed files with 146 additions and 162 deletions
|
|
@ -1,5 +1,25 @@
|
||||||
use crate::*;
|
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<Self> {
|
||||||
|
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<Self> {
|
||||||
|
$handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum NextPrev {
|
pub enum NextPrev {
|
||||||
Next,
|
Next,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,13 @@ pub struct GrooveboxTui {
|
||||||
pub split: u16,
|
pub split: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum GrooveboxCommand {
|
||||||
|
Sequencer(SequencerCommand),
|
||||||
|
Sampler(SamplerCommand),
|
||||||
|
}
|
||||||
|
|
||||||
render!(|self:GrooveboxTui|Bsp::s(Tui::fixed_y(self.split, &self.sequencer), &self.sampler));
|
render!(|self:GrooveboxTui|Bsp::s(Tui::fixed_y(self.split, &self.sequencer), &self.sampler));
|
||||||
audio!(|self:GrooveboxTui,_client,_process|Control::Continue);
|
audio!(|self:GrooveboxTui,_client,_process|Control::Continue);
|
||||||
handle!(<Tui>|self:GrooveboxTui,input|Ok(None));
|
handle!(<Tui>|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input));
|
||||||
|
command!(|self:GrooveboxCommand,state:GrooveboxTui|todo!());
|
||||||
|
input_to_command!(GrooveboxCommand: <Tui>|state:GrooveboxTui,input|todo!());
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ use symphonia::core::probe::Hint;
|
||||||
use symphonia::core::audio::SampleBuffer;
|
use symphonia::core::audio::SampleBuffer;
|
||||||
use symphonia::default::get_codecs;
|
use symphonia::default::get_codecs;
|
||||||
|
|
||||||
|
pub enum SamplerCommand {
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<&Arc<RwLock<JackClient>>> for SamplerTui {
|
impl TryFrom<&Arc<RwLock<JackClient>>> for SamplerTui {
|
||||||
type Error = Box<dyn std::error::Error>;
|
type Error = Box<dyn std::error::Error>;
|
||||||
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||||
|
|
@ -65,54 +68,55 @@ render!(|self: SamplerTui|{
|
||||||
})),
|
})),
|
||||||
|
|
||||||
col!([
|
col!([
|
||||||
"",
|
Tui::push_x(2, row!([
|
||||||
row!([
|
Tui::bold(true, "Sampler"), "|Voices: ",
|
||||||
" ", Tui::bold(true, "Sampler."), " Voices: ",
|
|
||||||
&format!("{}", self.state.voices.read().unwrap().len()),
|
&format!("{}", self.state.voices.read().unwrap().len()),
|
||||||
]),
|
])),
|
||||||
" ",
|
|
||||||
Tui::either(self.state.unmapped.len() > 0,
|
Tui::either(self.state.unmapped.len() > 0,
|
||||||
col!((index, sample) in self.state.unmapped.iter().enumerate() =>
|
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,
|
Tui::either(self.state.mapped.len() > 0,
|
||||||
col!((index, sample) in self.state.unmapped.iter().enumerate() =>
|
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!(<Tui>|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 {
|
impl SamplerTui {
|
||||||
/// Immutable reference to sample at cursor.
|
/// Immutable reference to sample at cursor.
|
||||||
pub fn sample (&self) -> Option<&Arc<RwLock<Sample>>> {
|
pub fn sample (&self) -> Option<&Arc<RwLock<Sample>>> {
|
||||||
|
|
@ -256,41 +260,6 @@ fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||||
todo!();
|
todo!();
|
||||||
}
|
}
|
||||||
|
|
||||||
handle!(<Tui>|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<OsString>, Vec<OsString>)> {
|
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
||||||
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
|
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
|
||||||
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
|
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,49 @@ pub enum SequencerCommand {
|
||||||
ShowPool(bool),
|
ShowPool(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command<SequencerTui> for SequencerCommand {
|
handle!(<Tui>|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input));
|
||||||
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
|
input_to_command!(SequencerCommand: <Tui>|state:SequencerTui,input|Some(match input.event() {
|
||||||
Ok(match self {
|
// 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
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
command!(|self: SequencerCommand, state: SequencerTui|match self {
|
||||||
Self::Phrases(cmd) => {
|
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 {
|
match cmd {
|
||||||
|
|
@ -90,64 +130,12 @@ impl Command<SequencerTui> for SequencerCommand {
|
||||||
state.show_pool = value;
|
state.show_pool = value;
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
|
|
||||||
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
|
|
||||||
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
|
|
||||||
},
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
has_size!(<Tui>|self:SequencerTui|&self.size);
|
has_size!(<Tui>|self:SequencerTui|&self.size);
|
||||||
has_clock!(|self:SequencerTui|&self.clock);
|
has_clock!(|self:SequencerTui|&self.clock);
|
||||||
has_phrases!(|self:SequencerTui|self.phrases.phrases);
|
has_phrases!(|self:SequencerTui|self.phrases.phrases);
|
||||||
has_editor!(|self:SequencerTui|self.editor);
|
has_editor!(|self:SequencerTui|self.editor);
|
||||||
handle!(<Tui>|self:SequencerTui,i|SequencerCommand::execute_with_state(self, i));
|
|
||||||
render!(|self: SequencerTui|{
|
render!(|self: SequencerTui|{
|
||||||
let w = self.size.w();
|
let w = self.size.w();
|
||||||
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue