mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
edn-based sequencer and groovebox! forgot how to do north
This commit is contained in:
parent
1b82a957aa
commit
f6ab777c82
8 changed files with 320 additions and 381 deletions
1
tek/src/groovebox.edn
Normal file
1
tek/src/groovebox.edn
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
(bsp/s :toolbar (bsp/s :sample (bsp/s :status (bsp/w :pool (bsp/e :sampler :editor)))))
|
||||||
|
|
@ -1,27 +1,101 @@
|
||||||
mod groovebox_command; pub use self::groovebox_command::*;
|
|
||||||
mod groovebox_tui; pub use self::groovebox_tui::*;
|
|
||||||
|
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right};
|
use self::GrooveboxCommand as Cmd;
|
||||||
|
use EdnItem::*;
|
||||||
use ClockCommand::{Play, Pause};
|
use ClockCommand::{Play, Pause};
|
||||||
use GrooveboxCommand as Cmd;
|
|
||||||
use MidiEditCommand::*;
|
use MidiEditCommand::*;
|
||||||
use MidiPoolCommand::*;
|
use MidiPoolCommand::*;
|
||||||
|
use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right};
|
||||||
|
use std::marker::ConstParamTy;
|
||||||
pub struct Groovebox {
|
pub struct Groovebox {
|
||||||
pub _jack: Arc<RwLock<JackConnection>>,
|
pub _jack: Arc<RwLock<JackConnection>>,
|
||||||
pub player: MidiPlayer,
|
pub player: MidiPlayer,
|
||||||
pub pool: PoolModel,
|
pub pool: PoolModel,
|
||||||
pub editor: MidiEditor,
|
pub editor: MidiEditor,
|
||||||
pub sampler: Sampler,
|
pub sampler: Sampler,
|
||||||
|
|
||||||
pub compact: bool,
|
pub compact: bool,
|
||||||
pub size: Measure<TuiOut>,
|
pub size: Measure<TuiOut>,
|
||||||
pub status: bool,
|
pub status: bool,
|
||||||
pub note_buf: Vec<u8>,
|
pub note_buf: Vec<u8>,
|
||||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||||
pub perf: PerfModel,
|
pub perf: PerfModel,
|
||||||
|
}
|
||||||
|
render!(TuiOut: (self: Groovebox) => self.size.of(EdnView::from_source(self, Self::EDN)));
|
||||||
|
// this works:
|
||||||
|
//render!(TuiOut: (self: Groovebox) => self.size.of(
|
||||||
|
//Bsp::s(self.toolbar_view(),
|
||||||
|
//Bsp::n(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 EdnViewData<TuiOut> for &Groovebox {
|
||||||
|
fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
|
||||||
|
use EdnItem::*;
|
||||||
|
match item {
|
||||||
|
Nil => Box::new(()),
|
||||||
|
Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
||||||
|
Sym(":editor") => (&self.editor).boxed(),
|
||||||
|
Sym(":pool") => self.pool_view().boxed(),
|
||||||
|
Sym(":status") => self.status_view().boxed(),
|
||||||
|
Sym(":toolbar") => self.toolbar_view().boxed(),
|
||||||
|
Sym(":sampler") => self.sampler_view().boxed(),
|
||||||
|
Sym(":sample") => self.sample_view().boxed(),
|
||||||
|
_ => panic!("no content for {item:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn get_unit (&self, item: EdnItem<&str>) -> u16 {
|
||||||
|
use EdnItem::*;
|
||||||
|
match item.to_str() {
|
||||||
|
":sample-h" => if self.compact { 0 } else { 5 },
|
||||||
|
":samples-w" => if self.compact { 4 } else { 11 },
|
||||||
|
":samples-y" => if self.compact { 1 } else { 0 },
|
||||||
|
":pool-w" => if self.compact { 5 } else {
|
||||||
|
let w = self.size.w();
|
||||||
|
if w > 60 { 20 } else if w > 40 { 15 } else { 10 }
|
||||||
|
},
|
||||||
|
_ => 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Groovebox {
|
||||||
|
const EDN: &'static str = include_str!("groovebox.edn");
|
||||||
|
fn toolbar_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
Fill::x(Fixed::y(2, lay!(
|
||||||
|
Fill::x(Align::w(Meter("L/", self.sampler.input_meter[0]))),
|
||||||
|
Fill::x(Align::e(Meter("R/", self.sampler.input_meter[1]))),
|
||||||
|
Align::x(TransportView::new(true, &self.player.clock)),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
fn status_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
row!(
|
||||||
|
self.player.play_status(),
|
||||||
|
self.player.next_status(),
|
||||||
|
self.editor.clip_status(),
|
||||||
|
self.editor.edit_status(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn sample_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
let note_pt = self.editor.note_point();
|
||||||
|
let sample_h = if self.compact { 0 } else { 5 };
|
||||||
|
Max::y(sample_h, Fill::xy(
|
||||||
|
Bsp::a(
|
||||||
|
Fill::x(Align::w(Fixed::y(1, SamplerStatus(&self.sampler, note_pt)))),
|
||||||
|
SampleViewer::from_sampler(&self.sampler, note_pt))))
|
||||||
|
}
|
||||||
|
fn pool_view (&self) -> impl Content<TuiOut> + 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 sampler_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
let note_pt = self.editor.note_point();
|
||||||
|
let sampler_w = if self.compact { 4 } else { 40 };
|
||||||
|
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))))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
audio!(|self: Groovebox, client, scope|{
|
audio!(|self: Groovebox, client, scope|{
|
||||||
|
|
@ -58,46 +132,216 @@ audio!(|self: Groovebox, client, scope|{
|
||||||
});
|
});
|
||||||
|
|
||||||
has_clock!(|self: Groovebox|self.player.clock());
|
has_clock!(|self: Groovebox|self.player.clock());
|
||||||
|
pub enum GrooveboxCommand {
|
||||||
impl EdnViewData<TuiOut> for &Groovebox {
|
Compact(bool),
|
||||||
fn get_unit (&self, item: EdnItem<&str>) -> u16 {
|
History(isize),
|
||||||
use EdnItem::*;
|
Clock(ClockCommand),
|
||||||
match item.to_str() {
|
Pool(PoolCommand),
|
||||||
":sample-h" => if self.compact { 0 } else { 5 },
|
Editor(MidiEditCommand),
|
||||||
":samples-w" => if self.compact { 4 } else { 11 },
|
Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
||||||
":samples-y" => if self.compact { 1 } else { 0 },
|
Sampler(SamplerCommand),
|
||||||
":pool-w" => if self.compact { 5 } else {
|
|
||||||
let w = self.size.w();
|
|
||||||
if w > 60 { 20 } else if w > 40 { 15 } else { 10 }
|
|
||||||
},
|
|
||||||
_ => 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn get_content <'a> (&'a self, item: EdnItem<&str>) -> RenderBox<'a, TuiOut> {
|
|
||||||
use EdnItem::*;
|
|
||||||
match item {
|
|
||||||
Nil => Box::new(()),
|
|
||||||
Sym(bol) => match bol {
|
|
||||||
":input-meter-l" => Meter("L/", self.sampler.input_meter[0]).boxed(),
|
|
||||||
":input-meter-r" => Box::new(Meter("R/", self.sampler.input_meter[1])),
|
|
||||||
|
|
||||||
":transport" => Box::new(TransportView::new(true, &self.player.clock)),
|
|
||||||
":clip-play" => Box::new(self.player.play_status()),
|
|
||||||
":clip-next" => Box::new(self.player.next_status()),
|
|
||||||
":clip-edit" => Box::new(self.editor.clip_status()),
|
|
||||||
":edit-stat" => Box::new(self.editor.edit_status()),
|
|
||||||
":pool-view" => Box::new(PoolView(self.compact, &self.pool)),
|
|
||||||
":midi-view" => Box::new(&self.editor),
|
|
||||||
|
|
||||||
":sample-view" => Box::new(SampleViewer::from_sampler(&self.sampler, self.editor.note_point())),
|
|
||||||
":sample-stat" => Box::new(SamplerStatus(&self.sampler, self.editor.note_point())),
|
|
||||||
":samples-view" => Box::new(SampleList::new(self.compact, &self.sampler, &self.editor)),
|
|
||||||
|
|
||||||
_ => panic!("unknown sym {bol:?}")
|
|
||||||
},
|
|
||||||
Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
|
||||||
_ => panic!("no content for {item:?}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
||||||
|
Self::Enqueue(clip) => {
|
||||||
|
state.player.enqueue_next(clip.as_ref());
|
||||||
|
None
|
||||||
|
},
|
||||||
|
Self::Pool(cmd) => match cmd {
|
||||||
|
// autoselect: automatically load selected clip in editor
|
||||||
|
PoolCommand::Select(_) => {
|
||||||
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
|
state.editor.set_clip(Some(state.pool.clip()));
|
||||||
|
undo
|
||||||
|
},
|
||||||
|
// update color in all places simultaneously
|
||||||
|
PoolCommand::Phrase(SetColor(index, _)) => {
|
||||||
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
|
state.editor.set_clip(Some(state.pool.clip()));
|
||||||
|
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!(TuiIn: |self: Groovebox, input|
|
||||||
|
GrooveboxCommand::execute_with_state(self, input.event()));
|
||||||
|
|
||||||
|
keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand {
|
||||||
|
// Tab: Toggle compact mode
|
||||||
|
key(Tab) => Cmd::Compact(!state.compact),
|
||||||
|
// q: Enqueue currently edited clip
|
||||||
|
key(Char('q')) => Cmd::Enqueue(Some(state.pool.clip().clone())),
|
||||||
|
// 0: Enqueue clip 0 (stop all)
|
||||||
|
key(Char('0')) => Cmd::Enqueue(Some(state.pool.clips()[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 clip
|
||||||
|
shift(key(Char('e'))) => if let Some((_, Some(playing))) = state.player.play_clip() {
|
||||||
|
let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone());
|
||||||
|
let selected = state.pool.clip().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
|
||||||
|
});
|
||||||
|
|
||||||
|
///// Status bar for sequencer app
|
||||||
|
//#[derive(Clone)]
|
||||||
|
//pub struct GrooveboxStatus {
|
||||||
|
//pub(crate) width: usize,
|
||||||
|
//pub(crate) cpu: Option<String>,
|
||||||
|
//pub(crate) size: String,
|
||||||
|
//pub(crate) playing: bool,
|
||||||
|
//}
|
||||||
|
//from!(|state: &Groovebox|GrooveboxStatus = {
|
||||||
|
//let samples = state.clock().chunk.load(Relaxed);
|
||||||
|
//let rate = state.clock().timebase.sr.get();
|
||||||
|
//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()),
|
||||||
|
//}
|
||||||
|
//});
|
||||||
|
//render!(TuiOut: (self: GrooveboxStatus) => Fixed::y(2, lay!(
|
||||||
|
//Self::help(),
|
||||||
|
//Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
|
||||||
|
//)));
|
||||||
|
//impl GrooveboxStatus {
|
||||||
|
//fn help () -> impl Content<TuiOut> {
|
||||||
|
//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"), ("Ctrl", "scroll"), ),
|
||||||
|
//double(("a", "append"), ("s", "set note"),),
|
||||||
|
//double((",.", "length"), ("<>", "triplet"), ),
|
||||||
|
//double(("[]", "phrase"), ("{}", "order"), ),
|
||||||
|
//double(("q", "enqueue"), ("e", "edit"), ),
|
||||||
|
//double(("c", "color"), ("", ""),),
|
||||||
|
//))
|
||||||
|
//}
|
||||||
|
//fn stats (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//row!(&self.cpu, &self.size)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//render!(TuiOut: (self: Groovebox) => self.size.of(
|
||||||
|
//Bsp::s(self.toolbar_view(),
|
||||||
|
//Bsp::n(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)))))))));
|
||||||
|
|
||||||
|
//const GROOVEBOX_EDN: &'static str = include_str!("groovebox.edn");
|
||||||
|
|
||||||
|
//impl Content<TuiOut> for Groovebox {
|
||||||
|
//fn content (&self) -> impl Content<TuiOut> {
|
||||||
|
//EdnView::parse(self.edn.as_slice())
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//macro_rules! edn_context {
|
||||||
|
//($Struct:ident |$l:lifetime, $state:ident| {
|
||||||
|
//$($key:literal = $field:ident: $Type:ty => $expr:expr,)*
|
||||||
|
//}) => {
|
||||||
|
|
||||||
|
//#[derive(Default)]
|
||||||
|
//pub struct EdnView<$l> { $($field: Option<$Type>),* }
|
||||||
|
|
||||||
|
//impl<$l> EdnView<$l> {
|
||||||
|
//pub fn parse <'e> (edn: &[Edn<'e>]) -> impl Fn(&$Struct) + use<'e> {
|
||||||
|
//let imports = Self::imports_all(edn);
|
||||||
|
//move |state| {
|
||||||
|
//let mut context = EdnView::default();
|
||||||
|
//for import in imports.iter() {
|
||||||
|
//context.import(state, import)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//fn imports_all <'e> (edn: &[Edn<'e>]) -> Vec<&'e str> {
|
||||||
|
//let mut imports = vec![];
|
||||||
|
//for edn in edn.iter() {
|
||||||
|
//for import in Self::imports_one(edn) {
|
||||||
|
//imports.push(import);
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//imports
|
||||||
|
//}
|
||||||
|
//fn imports_one <'e> (edn: &Edn<'e>) -> Vec<&'e str> {
|
||||||
|
//match edn {
|
||||||
|
//Edn::Symbol(import) => vec![import],
|
||||||
|
//Edn::List(edn) => Self::imports_all(edn.as_slice()),
|
||||||
|
//_ => vec![],
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//pub fn import (&mut self, $state: &$l$Struct, key: &str) {
|
||||||
|
//match key {
|
||||||
|
//$($key => self.$field = Some($expr),)*
|
||||||
|
//_ => {}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
////impl Groovebox {
|
||||||
|
////fn status_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
////let note_pt = self.editor.note_point();
|
||||||
|
////Align::w(Fixed::y(1, ))
|
||||||
|
////}
|
||||||
|
////fn pool_view (&self) -> impl Content<TuiOut> + 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 },
|
||||||
|
////)
|
||||||
|
////}
|
||||||
|
////fn sampler_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
////let sampler_w = if self.compact { 4 } else { 11 };
|
||||||
|
////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))))
|
||||||
|
////}
|
||||||
|
////}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
(bsp/s
|
|
||||||
(fill/x (fixed/y 2 (bsp/a
|
|
||||||
(align/x :transport)
|
|
||||||
(bsp/a
|
|
||||||
(align/w :input-meter-l)
|
|
||||||
(align/e :input-meter-r)))))
|
|
||||||
(bsp/n
|
|
||||||
(bsp/e :clip-play (bsp/e :clip-next (bsp/e :clip-edit :edit-stat)))
|
|
||||||
(bsp/n
|
|
||||||
(max/y :sample-h (fill/xy :sample-view))
|
|
||||||
(bsp/n
|
|
||||||
(align/w (fixed/y 1 :sample-stat))
|
|
||||||
(bsp/n
|
|
||||||
(fixed/x :pool-w :pool-view)
|
|
||||||
(fill/xy (bsp/e
|
|
||||||
(fixed/x :samples-w (push/y :samples-y :samples-view))
|
|
||||||
:midi-view)))))))
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
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(clip) => {
|
|
||||||
state.player.enqueue_next(clip.as_ref());
|
|
||||||
None
|
|
||||||
},
|
|
||||||
Self::Pool(cmd) => match cmd {
|
|
||||||
// autoselect: automatically load selected clip in editor
|
|
||||||
PoolCommand::Select(_) => {
|
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
|
||||||
state.editor.set_clip(Some(state.pool.clip()));
|
|
||||||
undo
|
|
||||||
},
|
|
||||||
// update color in all places simultaneously
|
|
||||||
PoolCommand::Phrase(SetColor(index, _)) => {
|
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
|
||||||
state.editor.set_clip(Some(state.pool.clip()));
|
|
||||||
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!(TuiIn: |self: Groovebox, input|
|
|
||||||
GrooveboxCommand::execute_with_state(self, input.event()));
|
|
||||||
|
|
||||||
keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand {
|
|
||||||
// Tab: Toggle compact mode
|
|
||||||
key(Tab) => Cmd::Compact(!state.compact),
|
|
||||||
// q: Enqueue currently edited clip
|
|
||||||
key(Char('q')) => Cmd::Enqueue(Some(state.pool.clip().clone())),
|
|
||||||
// 0: Enqueue clip 0 (stop all)
|
|
||||||
key(Char('0')) => Cmd::Enqueue(Some(state.pool.clips()[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 clip
|
|
||||||
shift(key(Char('e'))) => if let Some((_, Some(playing))) = state.player.play_clip() {
|
|
||||||
let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone());
|
|
||||||
let selected = state.pool.clip().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
|
|
||||||
});
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
use super::*;
|
|
||||||
use std::marker::ConstParamTy;
|
|
||||||
use EdnItem::*;
|
|
||||||
|
|
||||||
const EDN: &'static str = include_str!("groovebox.edn");
|
|
||||||
|
|
||||||
// this works:
|
|
||||||
render!(TuiOut: (self: Groovebox) => self.size.of(
|
|
||||||
Bsp::s(self.toolbar_view(),
|
|
||||||
Bsp::n(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)))))))));
|
|
||||||
|
|
||||||
// this almost does:
|
|
||||||
//impl Content<TuiOut> for Groovebox {
|
|
||||||
//fn content (&self) -> impl Render<TuiOut> {
|
|
||||||
//self.size.of(EdnView::from_source(self, EDN))
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
impl Groovebox {
|
|
||||||
fn toolbar_view (&self) -> impl Content<TuiOut> + 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(TransportView::new(true, &self.player.clock)),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
fn selector_view (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
row!(
|
|
||||||
self.player.play_status(),
|
|
||||||
self.player.next_status(),
|
|
||||||
self.editor.clip_status(),
|
|
||||||
self.editor.edit_status(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fn sample_view (&self) -> impl Content<TuiOut> + 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 status_view (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
let note_pt = self.editor.note_point();
|
|
||||||
Align::w(Fixed::y(1, SamplerStatus(&self.sampler, note_pt)))
|
|
||||||
}
|
|
||||||
fn pool_view (&self) -> impl Content<TuiOut> + 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 sampler_view (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
let sampler_w = if self.compact { 4 } else { 40 };
|
|
||||||
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))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///// Status bar for sequencer app
|
|
||||||
//#[derive(Clone)]
|
|
||||||
//pub struct GrooveboxStatus {
|
|
||||||
//pub(crate) width: usize,
|
|
||||||
//pub(crate) cpu: Option<String>,
|
|
||||||
//pub(crate) size: String,
|
|
||||||
//pub(crate) playing: bool,
|
|
||||||
//}
|
|
||||||
//from!(|state: &Groovebox|GrooveboxStatus = {
|
|
||||||
//let samples = state.clock().chunk.load(Relaxed);
|
|
||||||
//let rate = state.clock().timebase.sr.get();
|
|
||||||
//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()),
|
|
||||||
//}
|
|
||||||
//});
|
|
||||||
//render!(TuiOut: (self: GrooveboxStatus) => Fixed::y(2, lay!(
|
|
||||||
//Self::help(),
|
|
||||||
//Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
|
|
||||||
//)));
|
|
||||||
//impl GrooveboxStatus {
|
|
||||||
//fn help () -> impl Content<TuiOut> {
|
|
||||||
//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"), ("Ctrl", "scroll"), ),
|
|
||||||
//double(("a", "append"), ("s", "set note"),),
|
|
||||||
//double((",.", "length"), ("<>", "triplet"), ),
|
|
||||||
//double(("[]", "phrase"), ("{}", "order"), ),
|
|
||||||
//double(("q", "enqueue"), ("e", "edit"), ),
|
|
||||||
//double(("c", "color"), ("", ""),),
|
|
||||||
//))
|
|
||||||
//}
|
|
||||||
//fn stats (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
//row!(&self.cpu, &self.size)
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//render!(TuiOut: (self: Groovebox) => self.size.of(
|
|
||||||
//Bsp::s(self.toolbar_view(),
|
|
||||||
//Bsp::n(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)))))))));
|
|
||||||
|
|
||||||
//const GROOVEBOX_EDN: &'static str = include_str!("groovebox.edn");
|
|
||||||
|
|
||||||
//impl Content<TuiOut> for Groovebox {
|
|
||||||
//fn content (&self) -> impl Content<TuiOut> {
|
|
||||||
//EdnView::parse(self.edn.as_slice())
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
//macro_rules! edn_context {
|
|
||||||
//($Struct:ident |$l:lifetime, $state:ident| {
|
|
||||||
//$($key:literal = $field:ident: $Type:ty => $expr:expr,)*
|
|
||||||
//}) => {
|
|
||||||
|
|
||||||
//#[derive(Default)]
|
|
||||||
//pub struct EdnView<$l> { $($field: Option<$Type>),* }
|
|
||||||
|
|
||||||
//impl<$l> EdnView<$l> {
|
|
||||||
//pub fn parse <'e> (edn: &[Edn<'e>]) -> impl Fn(&$Struct) + use<'e> {
|
|
||||||
//let imports = Self::imports_all(edn);
|
|
||||||
//move |state| {
|
|
||||||
//let mut context = EdnView::default();
|
|
||||||
//for import in imports.iter() {
|
|
||||||
//context.import(state, import)
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//fn imports_all <'e> (edn: &[Edn<'e>]) -> Vec<&'e str> {
|
|
||||||
//let mut imports = vec![];
|
|
||||||
//for edn in edn.iter() {
|
|
||||||
//for import in Self::imports_one(edn) {
|
|
||||||
//imports.push(import);
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//imports
|
|
||||||
//}
|
|
||||||
//fn imports_one <'e> (edn: &Edn<'e>) -> Vec<&'e str> {
|
|
||||||
//match edn {
|
|
||||||
//Edn::Symbol(import) => vec![import],
|
|
||||||
//Edn::List(edn) => Self::imports_all(edn.as_slice()),
|
|
||||||
//_ => vec![],
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//pub fn import (&mut self, $state: &$l$Struct, key: &str) {
|
|
||||||
//match key {
|
|
||||||
//$($key => self.$field = Some($expr),)*
|
|
||||||
//_ => {}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
////impl Groovebox {
|
|
||||||
////fn status_view (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
////let note_pt = self.editor.note_point();
|
|
||||||
////Align::w(Fixed::y(1, ))
|
|
||||||
////}
|
|
||||||
////fn pool_view (&self) -> impl Content<TuiOut> + 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 },
|
|
||||||
////)
|
|
||||||
////}
|
|
||||||
////fn sampler_view (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
////let sampler_w = if self.compact { 4 } else { 11 };
|
|
||||||
////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))))
|
|
||||||
////}
|
|
||||||
////}
|
|
||||||
|
|
@ -4,10 +4,10 @@ pub struct PoolView<'a>(pub bool, pub &'a PoolModel);
|
||||||
render!(TuiOut: (self: PoolView<'a>) => {
|
render!(TuiOut: (self: PoolView<'a>) => {
|
||||||
let Self(compact, model) = self;
|
let Self(compact, model) = self;
|
||||||
let PoolModel { clips, mode, .. } = self.1;
|
let PoolModel { clips, mode, .. } = self.1;
|
||||||
let color = self.1.clip().read().unwrap().color;
|
let color = self.1.clip().read().unwrap().color;
|
||||||
Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, Outer(
|
let on_bg = |x|Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x));
|
||||||
Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)
|
let border = |x|Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x);
|
||||||
).enclose(Map::new(||model.clips().iter(), |clip, i|{
|
on_bg(border(Map::new(||model.clips().iter(), |clip, i|{
|
||||||
let item_height = 1;
|
let item_height = 1;
|
||||||
let item_offset = i as u16 * item_height;
|
let item_offset = i as u16 * item_height;
|
||||||
let selected = i == model.clip_index();
|
let selected = i == model.clip_index();
|
||||||
|
|
@ -15,10 +15,10 @@ render!(TuiOut: (self: PoolView<'a>) => {
|
||||||
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
||||||
let length = if *compact { String::default() } else { format!("{length} ") };
|
let length = if *compact { String::default() } else { format!("{length} ") };
|
||||||
map_south(item_offset, item_height, Tui::bg(if selected { color.light.rgb } else { color.base.rgb }, lay!(
|
map_south(item_offset, item_height, Tui::bg(if selected { color.light.rgb } else { color.base.rgb }, lay!(
|
||||||
Align::w(Tui::fg(color.lightest.rgb, Tui::bold(selected, name))),
|
Fill::x(Align::w(Tui::fg(color.lightest.rgb, Tui::bold(selected, name)))),
|
||||||
Align::e(Tui::fg(color.lightest.rgb, Tui::bold(selected, length))),
|
Fill::x(Align::e(Tui::fg(color.lightest.rgb, Tui::bold(selected, length)))),
|
||||||
Align::w(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "▶")))),
|
Fill::x(Align::w(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "▶"))))),
|
||||||
Align::e(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "◀")))),
|
Fill::x(Align::e(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "◀"))))),
|
||||||
)))
|
)))
|
||||||
}))))
|
})))
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
:editor
|
(bsp/s :toolbar (bsp/s :status (bsp/w :pool :editor)))
|
||||||
|
|
|
||||||
|
|
@ -27,40 +27,31 @@ impl EdnViewData<TuiOut> for &Sequencer {
|
||||||
fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
|
fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
|
||||||
use EdnItem::*;
|
use EdnItem::*;
|
||||||
match item {
|
match item {
|
||||||
Nil => Box::new(()),
|
Nil => Box::new(()),
|
||||||
Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
||||||
Sym(":editor") => (&self.editor).boxed(),
|
Sym(":editor") => (&self.editor).boxed(),
|
||||||
|
Sym(":pool") => self.pool_view().boxed(),
|
||||||
|
Sym(":status") => self.status_view().boxed(),
|
||||||
|
Sym(":toolbar") => self.toolbar_view().boxed(),
|
||||||
_ => panic!("no content for {item:?}")
|
_ => panic!("no content for {item:?}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
//render!(TuiOut: (self: Sequencer) => self.size.of(
|
|
||||||
//Bsp::s(self.toolbar_view(),
|
|
||||||
//Bsp::n(self.selector_view(),
|
|
||||||
//Bsp::n(self.status_view(),
|
|
||||||
//Bsp::w(self.pool_view(), Fill::xy(&self.editor)))))));
|
|
||||||
impl Sequencer {
|
impl Sequencer {
|
||||||
const EDN: &'static str = include_str!("sequencer.edn");
|
const EDN: &'static str = include_str!("sequencer.edn");
|
||||||
fn toolbar_view (&self) -> impl Content<TuiOut> + use<'_> {
|
fn toolbar_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.player.clock))))
|
Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.player.clock))))
|
||||||
}
|
}
|
||||||
fn status_view (&self) -> impl Content<TuiOut> + use<'_> {
|
fn status_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
row!(
|
Bsp::e(
|
||||||
When(self.selectors, Bsp::e(
|
When(self.selectors, Bsp::e(
|
||||||
self.player.play_status(),
|
self.player.play_status(),
|
||||||
self.player.next_status(),
|
self.player.next_status(),
|
||||||
)),
|
)),
|
||||||
self.editor.clip_status(),
|
Bsp::e(
|
||||||
self.editor.edit_status(),
|
self.editor.clip_status(),
|
||||||
)
|
self.editor.edit_status(),
|
||||||
}
|
)
|
||||||
fn selector_view (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
row!(
|
|
||||||
self.player.play_status(),
|
|
||||||
self.player.next_status(),
|
|
||||||
self.editor.clip_status(),
|
|
||||||
self.editor.edit_status(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
|
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue