mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-01 08:36:42 +01:00
finally, flatten arranger
This commit is contained in:
parent
8c6716adce
commit
06b643e2b1
14 changed files with 1193 additions and 1174 deletions
|
|
@ -646,13 +646,13 @@ impl Content<TuiOut> for AddSampleModal {
|
||||||
|
|
||||||
handle!(TuiIn: |self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input.event()));
|
handle!(TuiIn: |self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input.event()));
|
||||||
|
|
||||||
pub enum SamplerTuiCommand {
|
#[derive(Clone, Debug)] pub enum SamplerTuiCommand {
|
||||||
Import(FileBrowserCommand),
|
Import(FileBrowserCommand),
|
||||||
Select(usize),
|
Select(usize),
|
||||||
Sample(SamplerCommand),
|
Sample(SamplerCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum SamplerCommand {
|
#[derive(Clone, Debug)] pub enum SamplerCommand {
|
||||||
RecordBegin(u7),
|
RecordBegin(u7),
|
||||||
RecordCancel,
|
RecordCancel,
|
||||||
RecordFinish,
|
RecordFinish,
|
||||||
|
|
|
||||||
339
tek/src/app.rs
339
tek/src/app.rs
|
|
@ -87,342 +87,3 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render!(TuiOut: (self: App) => self.size.of(EdnView::from_source(self, self.edn.as_ref())));
|
|
||||||
audio!(|self: App, _client, _scope|Control::Continue);
|
|
||||||
impl EdnViewData<TuiOut> for &App {
|
|
||||||
fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
|
|
||||||
use EdnItem::*;
|
|
||||||
let w = self.tracks_with_sizes().last().map(|x|x.3 as u16).unwrap_or(0);
|
|
||||||
match item {
|
|
||||||
Nil => Box::new(()),
|
|
||||||
Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
|
||||||
Sym(":editor") => (&self.editor).boxed(),
|
|
||||||
Sym(":inputs") => self.input_row(w, 3).boxed(),
|
|
||||||
Sym(":outputs") => self.output_row(w, 3).boxed(),
|
|
||||||
Sym(":pool") => self.pool().boxed(),
|
|
||||||
Sym(":sample") => self.sample().boxed(),
|
|
||||||
Sym(":sampler") => self.sampler().boxed(),
|
|
||||||
Sym(":scenes") => self.scene_row(w, self.size.h().saturating_sub(9) as u16).boxed(),
|
|
||||||
Sym(":status") => self.status(0).boxed(),
|
|
||||||
Sym(":toolbar") => self.toolbar().boxed(),
|
|
||||||
Sym(":tracks") => self.track_row(w, 3).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 App {
|
|
||||||
fn compact (&self) -> bool { false }
|
|
||||||
fn toolbar (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock))))
|
|
||||||
}
|
|
||||||
fn status (&self, note_pt: usize) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
self.editor.as_ref()
|
|
||||||
.map(|e|Bsp::e(e.clip_status(), e.edit_status()))
|
|
||||||
}
|
|
||||||
fn pool (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
self.pool.as_ref()
|
|
||||||
.map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool))))
|
|
||||||
}
|
|
||||||
fn editor (&self) -> impl Content<TuiOut> + '_ {
|
|
||||||
&self.editor
|
|
||||||
}
|
|
||||||
fn sample <'a> (&'a self) -> impl Content<TuiOut> + 'a {
|
|
||||||
let compact = self.is_editing();
|
|
||||||
if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) {
|
|
||||||
let note_pt = editor.note_point();
|
|
||||||
let sample_h = if compact { 0 } else { 5 };
|
|
||||||
return Some(Max::y(sample_h, Fill::xy(Bsp::a(
|
|
||||||
Fill::x(Align::w(Fixed::y(1, self.status(note_pt)))),
|
|
||||||
sampler.viewer(note_pt)
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
fn sampler (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
let compact = self.is_editing();
|
|
||||||
if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) {
|
|
||||||
let note_pt = editor.note_point();
|
|
||||||
let w = if compact { 4 } else { 40 };
|
|
||||||
let y = if compact { 1 } else { 0 };
|
|
||||||
return Some(Fixed::x(w, Push::y(y, Fill::y(sampler.list(compact, editor)))))
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn row <'a> (
|
|
||||||
&'a self, w: u16, h: u16, a: impl Content<TuiOut> + 'a, b: impl Content<TuiOut> + 'a
|
|
||||||
) -> impl Content<TuiOut> + 'a {
|
|
||||||
Fixed::y(h, Bsp::e(
|
|
||||||
Fixed::xy(self.sidebar_w() as u16, h, a),
|
|
||||||
Fill::x(Align::c(Fixed::xy(w, h, b)))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
fn track_row (&self, w: u16, h: u16) -> impl Content<TuiOut> + '_ {
|
|
||||||
self.row(w, h, track_header(&self), track_cells(&self))
|
|
||||||
}
|
|
||||||
fn input_row (&self, w: u16, h: u16) -> impl Content<TuiOut> + '_ {
|
|
||||||
self.row(w, h, input_header(&self), input_cells(&self))
|
|
||||||
}
|
|
||||||
fn output_row (&self, w: u16, h: u16) -> impl Content<TuiOut> + '_ {
|
|
||||||
self.row(w, h, output_header(&self), output_cells(&self))
|
|
||||||
}
|
|
||||||
fn scene_row (&self, w: u16, h: u16) -> impl Content<TuiOut> + '_ {
|
|
||||||
self.row(w, h, output_header(&self), output_cells(&self))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tracks_with_sizes (&self)
|
|
||||||
-> impl Iterator<Item = (usize, &ArrangerTrack, usize, usize)>
|
|
||||||
{
|
|
||||||
tracks_with_sizes(self.tracks.iter(), match self.selected {
|
|
||||||
ArrangerSelection::Track(t) if self.is_editing() => Some(t),
|
|
||||||
ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t),
|
|
||||||
_ => None
|
|
||||||
}, self.editor_w())
|
|
||||||
}
|
|
||||||
pub fn scenes_with_sizes (&self, h: usize)
|
|
||||||
-> impl Iterator<Item = (usize, &ArrangerScene, usize, usize)>
|
|
||||||
{
|
|
||||||
scenes_with_sizes(self.scenes.iter(), &self.selected, self.is_editing(), 2, 15)
|
|
||||||
}
|
|
||||||
fn is_editing (&self) -> bool {
|
|
||||||
self.editing.load(Relaxed)
|
|
||||||
}
|
|
||||||
fn editor_w (&self) -> usize {
|
|
||||||
let editor = self.editor.as_ref().expect("missing editor");
|
|
||||||
(5 + (editor.time_len().get() / editor.time_zoom().get()))
|
|
||||||
.min(self.size.w().saturating_sub(20))
|
|
||||||
.max(16)
|
|
||||||
}
|
|
||||||
fn sidebar_w (&self) -> u16 {
|
|
||||||
let w = self.size.w();
|
|
||||||
let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
|
||||||
let w = if self.is_editing() { 8 } else { w };
|
|
||||||
w
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn scenes_with_sizes <'a>(
|
|
||||||
scenes: impl Iterator<Item=&'a ArrangerScene> + 'a,
|
|
||||||
selected: &'a ArrangerSelection,
|
|
||||||
editing: bool,
|
|
||||||
scene_height: usize,
|
|
||||||
scene_larger: usize,
|
|
||||||
) -> impl Iterator<Item = (usize, &'a ArrangerScene, usize, usize)> + 'a {
|
|
||||||
let mut y = 0;
|
|
||||||
let (selected_track, selected_scene) = match selected {
|
|
||||||
ArrangerSelection::Clip(t, s) => (Some(t), Some(s)),
|
|
||||||
_ => (None, None)
|
|
||||||
};
|
|
||||||
scenes.enumerate().map(move|(s, scene)|{
|
|
||||||
let active = editing && selected_track.is_some() && selected_scene == Some(&s);
|
|
||||||
let height = if active { scene_larger } else { scene_height };
|
|
||||||
let data = (s, scene, y, y + height);
|
|
||||||
y += height;
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn tracks_with_sizes <'a> (
|
|
||||||
tracks: impl Iterator<Item=&'a ArrangerTrack>,
|
|
||||||
active: Option<usize>,
|
|
||||||
bigger: usize
|
|
||||||
) -> impl Iterator<Item=(usize,&'a ArrangerTrack,usize,usize)> {
|
|
||||||
let mut x = 0;
|
|
||||||
tracks.enumerate().map(move |(index, track)|{
|
|
||||||
let width = if Some(index) == active { bigger } else { track.width.max(8) };
|
|
||||||
let data = (index, track, x, x + width);
|
|
||||||
x += width;
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn track_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
|
||||||
(||Tui::bg(TuiTheme::g(32), Tui::bold(true, Bsp::s(
|
|
||||||
row!(
|
|
||||||
Tui::fg(TuiTheme::g(128), "add "),
|
|
||||||
Tui::fg(TuiTheme::orange(), "t"),
|
|
||||||
Tui::fg(TuiTheme::g(128), "rack"),
|
|
||||||
),
|
|
||||||
row!(
|
|
||||||
Tui::fg(TuiTheme::orange(), "a"),
|
|
||||||
Tui::fg(TuiTheme::g(128), "dd scene"),
|
|
||||||
),
|
|
||||||
))).boxed()).into()
|
|
||||||
}
|
|
||||||
pub fn track_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
|
||||||
let iter = ||state.tracks_with_sizes();
|
|
||||||
(move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| {
|
|
||||||
let name = Push::x(1, &track.name);
|
|
||||||
let color = track.color();
|
|
||||||
let fg = color.lightest.rgb;
|
|
||||||
let bg = color.base.rgb;
|
|
||||||
let active = state.selected.track() == Some(i);
|
|
||||||
let bfg = if active { Color::Rgb(255,255,255) } else { Color::Rgb(0,0,0) };
|
|
||||||
let border = Style::default().fg(bfg).bg(bg);
|
|
||||||
Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16,
|
|
||||||
Outer(border).enclose(Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::x(name)))))
|
|
||||||
))
|
|
||||||
})).boxed()).into()
|
|
||||||
}
|
|
||||||
fn help_tag <'a>(before: &'a str, key: &'a str, after: &'a str) -> impl Content<TuiOut> + 'a {
|
|
||||||
let lo = TuiTheme::g(128);
|
|
||||||
let hi = TuiTheme::orange();
|
|
||||||
Tui::bold(true, row!(Tui::fg(lo, before), Tui::fg(hi, key), Tui::fg(lo, after)))
|
|
||||||
}
|
|
||||||
fn input_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
|
||||||
let fg = TuiTheme::g(224);
|
|
||||||
let bg = TuiTheme::g(64);
|
|
||||||
(move||Bsp::s(help_tag("midi ", "I", "ns"), state.midi_ins.get(0).map(|inp|Bsp::s(
|
|
||||||
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(inp.name.clone())))),
|
|
||||||
inp.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false,
|
|
||||||
Tui::fg_bg(fg, bg, connect.info()))))),
|
|
||||||
))).boxed()).into()
|
|
||||||
}
|
|
||||||
fn input_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
|
||||||
(move||Align::x(Map::new(||state.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
|
||||||
let w = (x2 - x1) as u16;
|
|
||||||
let color: ItemPalette = track.color().dark.into();
|
|
||||||
map_east(x1 as u16, w, Fixed::x(w, cell(color, Bsp::n(
|
|
||||||
rec_mon(color.base.rgb, false, false),
|
|
||||||
phat_hi(color.base.rgb, color.dark.rgb)
|
|
||||||
))))
|
|
||||||
})).boxed()).into()
|
|
||||||
}
|
|
||||||
fn output_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
|
||||||
let fg = TuiTheme::g(224);
|
|
||||||
let bg = TuiTheme::g(64);
|
|
||||||
(move||Bsp::s(help_tag("midi ", "O", "uts"), state.midi_outs.get(0).map(|out|Bsp::s(
|
|
||||||
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(out.name.clone())))),
|
|
||||||
out.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false,
|
|
||||||
Tui::fg_bg(fg, bg, connect.info()))))),
|
|
||||||
))).boxed()).into()
|
|
||||||
}
|
|
||||||
fn output_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
|
||||||
(move||Align::x(Map::new(||state.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
|
||||||
let w = (x2 - x1) as u16;
|
|
||||||
let color: ItemPalette = track.color().dark.into();
|
|
||||||
map_east(x1 as u16, w, Fixed::x(w, cell(color, Bsp::n(
|
|
||||||
mute_solo(color.base.rgb, false, false),
|
|
||||||
phat_hi(color.dark.rgb, color.darker.rgb)
|
|
||||||
))))
|
|
||||||
})).boxed()).into()
|
|
||||||
}
|
|
||||||
fn scene_headers <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
|
||||||
(||{
|
|
||||||
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
|
|
||||||
let selected = state.selected.scene();
|
|
||||||
Fill::y(Align::c(Map::new(||state.scenes_with_sizes(2), move|(_, scene, y1, y2), i| {
|
|
||||||
let h = (y2 - y1) as u16;
|
|
||||||
let name = format!("🭬{}", &scene.name);
|
|
||||||
let color = scene.color();
|
|
||||||
let active = selected == Some(i);
|
|
||||||
let mid = if active { color.light } else { color.base };
|
|
||||||
let top = Some(last_color.read().unwrap().base.rgb);
|
|
||||||
let cell = phat_sel_3(
|
|
||||||
active,
|
|
||||||
Tui::bold(true, name.clone()),
|
|
||||||
Tui::bold(true, name),
|
|
||||||
top,
|
|
||||||
mid.rgb,
|
|
||||||
Color::Rgb(0, 0, 0)
|
|
||||||
);
|
|
||||||
*last_color.write().unwrap() = color;
|
|
||||||
map_south(y1 as u16, h + 1, Fixed::y(h + 1, cell))
|
|
||||||
}))).boxed()
|
|
||||||
}).into()
|
|
||||||
}
|
|
||||||
fn scene_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
|
||||||
let editing = state.is_editing();
|
|
||||||
let tracks = move||state.tracks_with_sizes();
|
|
||||||
let scenes = ||state.scenes_with_sizes(2);
|
|
||||||
let selected_track = state.selected.track();
|
|
||||||
let selected_scene = state.selected.scene();
|
|
||||||
(move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| {
|
|
||||||
let w = (x2 - x1) as u16;
|
|
||||||
let color: ItemPalette = track.color().dark.into();
|
|
||||||
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
|
|
||||||
let cells = Map::new(scenes, move|(_, scene, y1, y2), s| {
|
|
||||||
let h = (y2 - y1) as u16;
|
|
||||||
let color = scene.color();
|
|
||||||
let (name, fg, bg) = if let Some(c) = &scene.clips[t] {
|
|
||||||
let c = c.read().unwrap();
|
|
||||||
(c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb)
|
|
||||||
} else {
|
|
||||||
("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32))
|
|
||||||
};
|
|
||||||
let last = last_color.read().unwrap().clone();
|
|
||||||
let active = editing && selected_scene == Some(s) && selected_track == Some(t);
|
|
||||||
let editor = Thunk::new(||&state.editor);
|
|
||||||
let cell = Thunk::new(move||phat_sel_3(
|
|
||||||
selected_track == Some(t) && selected_scene == Some(s),
|
|
||||||
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
|
|
||||||
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
|
|
||||||
if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(bg.into())
|
|
||||||
},
|
|
||||||
bg.into(),
|
|
||||||
bg.into(),
|
|
||||||
));
|
|
||||||
let cell = Either(active, editor, cell);
|
|
||||||
*last_color.write().unwrap() = bg.into();
|
|
||||||
map_south(
|
|
||||||
y1 as u16,
|
|
||||||
h + 1,
|
|
||||||
Fill::x(Fixed::y(h + 1, cell))
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed());
|
|
||||||
Fixed::x(w, map_east(x1 as u16, w, column))
|
|
||||||
}))).boxed()).into()
|
|
||||||
}
|
|
||||||
fn cell_clip <'a> (
|
|
||||||
scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16
|
|
||||||
) -> impl Content<TuiOut> + use<'a> {
|
|
||||||
scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{
|
|
||||||
let clip = clip.read().unwrap();
|
|
||||||
let mut bg = TuiTheme::border_bg();
|
|
||||||
let name = clip.name.to_string();
|
|
||||||
let max_w = name.len().min((w as usize).saturating_sub(2));
|
|
||||||
let color = clip.color;
|
|
||||||
bg = color.dark.rgb;
|
|
||||||
if let Some((_, Some(ref playing))) = track.player.play_clip() {
|
|
||||||
if *playing.read().unwrap() == *clip {
|
|
||||||
bg = color.light.rgb
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Fixed::xy(w, h, &Tui::bg(bg, Push::x(1, Fixed::x(w, &name.as_str()[0..max_w]))));
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
fn rec_mon (bg: Color, rec: bool, mon: bool) -> impl Content<TuiOut> {
|
|
||||||
row!(
|
|
||||||
Tui::fg_bg(if rec { Color::Red } else { bg }, bg, "▐"),
|
|
||||||
Tui::fg_bg(if rec { Color::White } else { Color::Rgb(0,0,0) }, bg, "REC"),
|
|
||||||
Tui::fg_bg(if rec { Color::White } else { bg }, bg, "▐"),
|
|
||||||
Tui::fg_bg(if mon { Color::White } else { Color::Rgb(0,0,0) }, bg, "MON"),
|
|
||||||
Tui::fg_bg(if mon { Color::White } else { bg }, bg, "▌"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content<TuiOut> {
|
|
||||||
row!(
|
|
||||||
Tui::fg_bg(if mute { Color::White } else { Color::Rgb(0,0,0) }, bg, "MUTE"),
|
|
||||||
Tui::fg_bg(if mute { Color::White } else { bg }, bg, "▐"),
|
|
||||||
Tui::fg_bg(if solo { Color::White } else { Color::Rgb(0,0,0) }, bg, "SOLO"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fn cell <T: Content<TuiOut>> (color: ItemPalette, field: T) -> impl Content<TuiOut> {
|
|
||||||
Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field))
|
|
||||||
}
|
|
||||||
|
|
||||||
handle!(TuiIn: |self: App, input| Ok(None));
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
mod arranger_scene; pub use self::arranger_scene::*;
|
|
||||||
mod arranger_select; pub use self::arranger_select::*;
|
|
||||||
mod arranger_track; pub use self::arranger_track::*;
|
|
||||||
mod arranger_h;
|
|
||||||
use ClockCommand::{Play, Pause};
|
use ClockCommand::{Play, Pause};
|
||||||
use self::ArrangerCommand as Cmd;
|
use self::ArrangerCommand as Cmd;
|
||||||
/// Root view for standalone `tek_arranger`
|
/// Root view for standalone `tek_arranger`
|
||||||
|
|
@ -48,7 +44,7 @@ impl EdnViewData<TuiOut> for &Arranger {
|
||||||
impl Arranger {
|
impl Arranger {
|
||||||
const EDN: &'static str = include_str!("arranger.edn");
|
const EDN: &'static str = include_str!("arranger.edn");
|
||||||
pub const LEFT_SEP: char = '▎';
|
pub const LEFT_SEP: char = '▎';
|
||||||
pub const TRACK_MIN_WIDTH: usize = 4;
|
pub const TRACK_MIN_WIDTH: usize = 9;
|
||||||
|
|
||||||
fn toolbar (&self) -> impl Content<TuiOut> + use<'_> {
|
fn toolbar (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock))))
|
Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock))))
|
||||||
|
|
@ -115,8 +111,8 @@ impl Arranger {
|
||||||
}
|
}
|
||||||
fn play_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
fn play_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
||||||
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
||||||
//let color = track.color();
|
//let color = track.color;
|
||||||
let color: ItemPalette = track.color().dark.into();
|
let color: ItemPalette = track.color.dark.into();
|
||||||
let timebase = self.clock().timebase();
|
let timebase = self.clock().timebase();
|
||||||
let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb,
|
let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb,
|
||||||
if let Some((_, Some(clip))) = track.player.play_clip().as_ref() {
|
if let Some((_, Some(clip))) = track.player.play_clip().as_ref() {
|
||||||
|
|
@ -143,8 +139,8 @@ impl Arranger {
|
||||||
}
|
}
|
||||||
fn next_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
fn next_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
||||||
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
||||||
let color: ItemPalette = track.color();
|
let color: ItemPalette = track.color;
|
||||||
let color: ItemPalette = track.color().dark.into();
|
let color: ItemPalette = track.color.dark.into();
|
||||||
let current = &self.clock().playhead;
|
let current = &self.clock().playhead;
|
||||||
let timebase = ¤t.timebase;
|
let timebase = ¤t.timebase;
|
||||||
let cell = Self::cell(color, Tui::bold(true, {
|
let cell = Self::cell(color, Tui::bold(true, {
|
||||||
|
|
@ -204,7 +200,7 @@ impl Arranger {
|
||||||
let iter = ||self.tracks_with_sizes();
|
let iter = ||self.tracks_with_sizes();
|
||||||
(move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| {
|
(move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| {
|
||||||
let name = Push::x(1, &track.name);
|
let name = Push::x(1, &track.name);
|
||||||
let color = track.color();
|
let color = track.color;
|
||||||
let fg = color.lightest.rgb;
|
let fg = color.lightest.rgb;
|
||||||
let bg = color.base.rgb;
|
let bg = color.base.rgb;
|
||||||
let active = self.selected.track() == Some(i);
|
let active = self.selected.track() == Some(i);
|
||||||
|
|
@ -243,7 +239,7 @@ impl Arranger {
|
||||||
fn input_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
fn input_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
||||||
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
||||||
let w = (x2 - x1) as u16;
|
let w = (x2 - x1) as u16;
|
||||||
let color: ItemPalette = track.color().dark.into();
|
let color: ItemPalette = track.color.dark.into();
|
||||||
map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n(
|
map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n(
|
||||||
Self::rec_mon(color.base.rgb, false, false),
|
Self::rec_mon(color.base.rgb, false, false),
|
||||||
phat_hi(color.base.rgb, color.dark.rgb)
|
phat_hi(color.base.rgb, color.dark.rgb)
|
||||||
|
|
@ -287,7 +283,7 @@ impl Arranger {
|
||||||
fn output_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
fn output_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
|
||||||
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
||||||
let w = (x2 - x1) as u16;
|
let w = (x2 - x1) as u16;
|
||||||
let color: ItemPalette = track.color().dark.into();
|
let color: ItemPalette = track.color.dark.into();
|
||||||
map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n(
|
map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n(
|
||||||
Self::mute_solo(color.base.rgb, false, false),
|
Self::mute_solo(color.base.rgb, false, false),
|
||||||
phat_hi(color.dark.rgb, color.darker.rgb)
|
phat_hi(color.dark.rgb, color.darker.rgb)
|
||||||
|
|
@ -317,7 +313,7 @@ impl Arranger {
|
||||||
Fill::y(Align::c(Map::new(||self.scenes_with_sizes(2), move|(_, scene, y1, y2), i| {
|
Fill::y(Align::c(Map::new(||self.scenes_with_sizes(2), move|(_, scene, y1, y2), i| {
|
||||||
let h = (y2 - y1) as u16;
|
let h = (y2 - y1) as u16;
|
||||||
let name = format!("🭬{}", &scene.name);
|
let name = format!("🭬{}", &scene.name);
|
||||||
let color = scene.color();
|
let color = scene.color;
|
||||||
let active = selected == Some(i);
|
let active = selected == Some(i);
|
||||||
let mid = if active { color.light } else { color.base };
|
let mid = if active { color.light } else { color.base };
|
||||||
let top = Some(last_color.read().unwrap().base.rgb);
|
let top = Some(last_color.read().unwrap().base.rgb);
|
||||||
|
|
@ -342,11 +338,11 @@ impl Arranger {
|
||||||
let selected_scene = self.selected.scene();
|
let selected_scene = self.selected.scene();
|
||||||
(move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| {
|
(move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| {
|
||||||
let w = (x2 - x1) as u16;
|
let w = (x2 - x1) as u16;
|
||||||
let color: ItemPalette = track.color().dark.into();
|
let color: ItemPalette = track.color.dark.into();
|
||||||
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
|
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
|
||||||
let cells = Map::new(scenes, move|(_, scene, y1, y2), s| {
|
let cells = Map::new(scenes, move|(_, scene, y1, y2), s| {
|
||||||
let h = (y2 - y1) as u16;
|
let h = (y2 - y1) as u16;
|
||||||
let color = scene.color();
|
let color = scene.color;
|
||||||
let (name, fg, bg) = if let Some(c) = &scene.clips[t] {
|
let (name, fg, bg) = if let Some(c) = &scene.clips[t] {
|
||||||
let c = c.read().unwrap();
|
let c = c.read().unwrap();
|
||||||
(c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb)
|
(c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb)
|
||||||
|
|
@ -559,46 +555,9 @@ pub fn phat_sel_3 <T: Content<TuiOut>> (
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
audio!(|self: Arranger, 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 sequencers
|
|
||||||
//let tracks = &mut self.tracks;
|
|
||||||
//let note_buf = &mut self.note_buf;
|
|
||||||
//let midi_buf = &mut self.midi_buf;
|
|
||||||
//if Control::Quit == TracksAudio(tracks, note_buf, midi_buf).process(client, scope) {
|
|
||||||
//return Control::Quit
|
|
||||||
//}
|
|
||||||
// FIXME: one of these per playing track
|
|
||||||
//self.now.set(0.);
|
|
||||||
//if let ArrangerSelection::Clip(t, s) = self.selected {
|
|
||||||
//let clip = self.scenes.get(s).map(|scene|scene.clips.get(t));
|
|
||||||
//if let Some(Some(Some(clip))) = clip {
|
|
||||||
//if let Some(track) = self.tracks().get(t) {
|
|
||||||
//if let Some((ref started_at, Some(ref playing))) = track.player.play_clip {
|
|
||||||
//let clip = clip.read().unwrap();
|
|
||||||
//if *playing.read().unwrap() == *clip {
|
|
||||||
//let pulse = self.current().pulse.get();
|
|
||||||
//let start = started_at.pulse.get();
|
|
||||||
//let now = (pulse - start) % clip.length as f64;
|
|
||||||
//self.now.set(now);
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
// End profiling cycle
|
|
||||||
self.perf.update(t0, scope);
|
|
||||||
return Control::Continue
|
|
||||||
});
|
|
||||||
has_clock!(|self: Arranger|&self.clock);
|
has_clock!(|self: Arranger|&self.clock);
|
||||||
has_clips!(|self: Arranger|self.pool.clips);
|
has_clips!(|self: Arranger|self.pool.clips);
|
||||||
has_editor!(|self: Arranger|self.editor);
|
has_editor!(|self: Arranger|self.editor);
|
||||||
handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event()));
|
|
||||||
impl Arranger {
|
impl Arranger {
|
||||||
pub fn activate (&mut self) -> Usually<()> {
|
pub fn activate (&mut self) -> Usually<()> {
|
||||||
if let ArrangerSelection::Scene(s) = self.selected {
|
if let ArrangerSelection::Scene(s) = self.selected {
|
||||||
|
|
@ -636,213 +595,6 @@ impl Arranger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Clone, Debug)] pub enum ArrangerCommand {
|
|
||||||
History(isize),
|
|
||||||
Color(ItemPalette),
|
|
||||||
Clock(ClockCommand),
|
|
||||||
Scene(ArrangerSceneCommand),
|
|
||||||
Track(ArrangerTrackCommand),
|
|
||||||
Clip(ArrangerClipCommand),
|
|
||||||
Select(ArrangerSelection),
|
|
||||||
Zoom(usize),
|
|
||||||
Phrases(PoolCommand),
|
|
||||||
Editor(MidiEditCommand),
|
|
||||||
StopAll,
|
|
||||||
Clear,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum ArrangerClipCommand {
|
|
||||||
Get(usize, usize),
|
|
||||||
Put(usize, usize, Option<Arc<RwLock<MidiClip>>>),
|
|
||||||
Enqueue(usize, usize),
|
|
||||||
Edit(Option<Arc<RwLock<MidiClip>>>),
|
|
||||||
SetLoop(usize, usize, bool),
|
|
||||||
SetColor(usize, usize, ItemPalette),
|
|
||||||
}
|
|
||||||
keymap!(KEYS_ARRANGER = |state: Arranger, input: Event| ArrangerCommand {
|
|
||||||
key(Char('u')) => Cmd::History(-1),
|
|
||||||
key(Char('U')) => Cmd::History(1),
|
|
||||||
// TODO: k: toggle on-screen keyboard
|
|
||||||
ctrl(key(Char('k'))) => { todo!("keyboard") },
|
|
||||||
// Transport: Play/pause
|
|
||||||
key(Char(' ')) => Cmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
|
|
||||||
// Transport: Play from start or rewind to start
|
|
||||||
shift(key(Char(' '))) => Cmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
|
|
||||||
key(Char('e')) => Cmd::Editor(MidiEditCommand::Show(state.pool.clip().clone())),
|
|
||||||
ctrl(key(Char('a'))) => Cmd::Scene(ArrangerSceneCommand::Add),
|
|
||||||
ctrl(key(Char('A'))) => return None,//Cmd::Scene(ArrangerSceneCommand::Add),
|
|
||||||
ctrl(key(Char('t'))) => Cmd::Track(ArrangerTrackCommand::Add),
|
|
||||||
// Tab: Toggle visibility of clip pool column
|
|
||||||
key(Tab) => Cmd::Phrases(PoolCommand::Show(!state.pool.visible)),
|
|
||||||
}, {
|
|
||||||
use ArrangerSelection as Selected;
|
|
||||||
use ArrangerSceneCommand as Scene;
|
|
||||||
use ArrangerTrackCommand as Track;
|
|
||||||
use ArrangerClipCommand as Clip;
|
|
||||||
let t_len = state.tracks.len();
|
|
||||||
let s_len = state.scenes.len();
|
|
||||||
match state.selected {
|
|
||||||
Selected::Clip(t, s) => clip_keymap(state, input, t, s),
|
|
||||||
Selected::Scene(s) => scene_keymap(state, input, s),
|
|
||||||
Selected::Track(t) => track_keymap(state, input, t),
|
|
||||||
Selected::Mix => match input {
|
|
||||||
|
|
||||||
kpat!(Delete) => Some(Cmd::Clear),
|
|
||||||
kpat!(Char('0')) => Some(Cmd::StopAll),
|
|
||||||
kpat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())),
|
|
||||||
|
|
||||||
kpat!(Up) => return None,
|
|
||||||
kpat!(Down) => Some( Cmd::Select(Selected::Scene(0))),
|
|
||||||
kpat!(Left) => return None,
|
|
||||||
kpat!(Right) => Some( Cmd::Select(Selected::Track(0))),
|
|
||||||
|
|
||||||
_ => None
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}.or_else(||if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) {
|
|
||||||
Some(Cmd::Editor(command))
|
|
||||||
} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
|
|
||||||
Some(Cmd::Phrases(command))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
})?);
|
|
||||||
|
|
||||||
fn clip_keymap (state: &Arranger, input: &Event, t: usize, s: usize) -> Option<ArrangerCommand> {
|
|
||||||
use ArrangerSelection as Selected;
|
|
||||||
use ArrangerSceneCommand as Scene;
|
|
||||||
use ArrangerTrackCommand as Track;
|
|
||||||
use ArrangerClipCommand as Clip;
|
|
||||||
let t_len = state.tracks.len();
|
|
||||||
let s_len = state.scenes.len();
|
|
||||||
Some(match input {
|
|
||||||
|
|
||||||
kpat!(Char('g')) => Cmd::Phrases(PoolCommand::Select(0)),
|
|
||||||
kpat!(Char('q')) => Cmd::Clip(Clip::Enqueue(t, s)),
|
|
||||||
kpat!(Char('l')) => Cmd::Clip(Clip::SetLoop(t, s, false)),
|
|
||||||
|
|
||||||
kpat!(Enter) => if state.scenes[s].clips[t].is_none() {
|
|
||||||
// FIXME: get this clip from the pool (autoregister via intmut)
|
|
||||||
let (_, clip) = state.add_clip();
|
|
||||||
Cmd::Clip(Clip::Put(t, s, Some(clip)))
|
|
||||||
} else {
|
|
||||||
return None
|
|
||||||
},
|
|
||||||
kpat!(Delete) => Cmd::Clip(Clip::Put(t, s, None)),
|
|
||||||
kpat!(Char('p')) => Cmd::Clip(Clip::Put(t, s, state.pool.clip().clone())),
|
|
||||||
kpat!(Char(',')) => Cmd::Clip(Clip::Put(t, s, None)),
|
|
||||||
kpat!(Char('.')) => Cmd::Clip(Clip::Put(t, s, None)),
|
|
||||||
kpat!(Char('<')) => Cmd::Clip(Clip::Put(t, s, None)),
|
|
||||||
kpat!(Char('>')) => Cmd::Clip(Clip::Put(t, s, None)),
|
|
||||||
|
|
||||||
kpat!(Up) => Cmd::Select(if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) }),
|
|
||||||
kpat!(Down) => Cmd::Select(Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1)))),
|
|
||||||
kpat!(Left) => Cmd::Select(if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) }),
|
|
||||||
kpat!(Right) => Cmd::Select(Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s)),
|
|
||||||
|
|
||||||
_ => return None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn scene_keymap (state: &Arranger, input: &Event, s: usize) -> Option<ArrangerCommand> {
|
|
||||||
use ArrangerSelection as Selected;
|
|
||||||
use ArrangerSceneCommand as Scene;
|
|
||||||
use ArrangerTrackCommand as Track;
|
|
||||||
use ArrangerClipCommand as Clip;
|
|
||||||
let t_len = state.tracks.len();
|
|
||||||
let s_len = state.scenes.len();
|
|
||||||
Some(match input {
|
|
||||||
|
|
||||||
kpat!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)),
|
|
||||||
kpat!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)),
|
|
||||||
kpat!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)),
|
|
||||||
kpat!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)),
|
|
||||||
kpat!(Char('q')) => Cmd::Scene(Scene::Enqueue(s)),
|
|
||||||
kpat!(Delete) => Cmd::Scene(Scene::Delete(s)),
|
|
||||||
kpat!(Char('c')) => Cmd::Scene(Scene::SetColor(s, ItemPalette::random())),
|
|
||||||
|
|
||||||
kpat!(Up) => Cmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix }),
|
|
||||||
kpat!(Down) => Cmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1)))),
|
|
||||||
kpat!(Left) => return None,
|
|
||||||
kpat!(Right) => Cmd::Select(Selected::Clip(0, s)),
|
|
||||||
|
|
||||||
_ => return None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn track_keymap (state: &Arranger, input: &Event, t: usize) -> Option<ArrangerCommand> {
|
|
||||||
use ArrangerSelection as Selected;
|
|
||||||
use ArrangerSceneCommand as Scene;
|
|
||||||
use ArrangerTrackCommand as Track;
|
|
||||||
use ArrangerClipCommand as Clip;
|
|
||||||
let t_len = state.tracks.len();
|
|
||||||
let s_len = state.scenes.len();
|
|
||||||
Some(match input {
|
|
||||||
|
|
||||||
kpat!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)),
|
|
||||||
kpat!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)),
|
|
||||||
kpat!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)),
|
|
||||||
kpat!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)),
|
|
||||||
kpat!(Delete) => Cmd::Track(Track::Delete(t)),
|
|
||||||
kpat!(Char('c')) => Cmd::Track(Track::SetColor(t, ItemPalette::random())),
|
|
||||||
|
|
||||||
kpat!(Up) => return None,
|
|
||||||
kpat!(Down) => Cmd::Select(Selected::Clip(t, 0)),
|
|
||||||
kpat!(Left) => Cmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix }),
|
|
||||||
kpat!(Right) => Cmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1)))),
|
|
||||||
|
|
||||||
_ => return None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
command!(|self: ArrangerCommand, state: Arranger|match self {
|
|
||||||
Self::Clear => { todo!() },
|
|
||||||
Self::History(_) => { todo!() },
|
|
||||||
Self::Zoom(_) => { todo!(); },
|
|
||||||
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
|
||||||
Self::Clip(cmd) => cmd.delegate(state, Self::Clip)?,
|
|
||||||
Self::Scene(cmd) => cmd.delegate(state, Self::Scene)?,
|
|
||||||
Self::Track(cmd) => cmd.delegate(state, Self::Track)?,
|
|
||||||
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
|
||||||
Self::Select(selected) => { state.selected = selected; None },
|
|
||||||
Self::StopAll => {
|
|
||||||
for track in 0..state.tracks.len() { state.tracks[track].player.enqueue_next(None); }
|
|
||||||
None
|
|
||||||
},
|
|
||||||
Self::Color(palette) => {
|
|
||||||
let old = state.color;
|
|
||||||
state.color = palette;
|
|
||||||
Some(Self::Color(old))
|
|
||||||
},
|
|
||||||
Self::Phrases(cmd) => {
|
|
||||||
match cmd {
|
|
||||||
// autoselect: automatically load selected clip in editor
|
|
||||||
PoolCommand::Select(_) => {
|
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Phrases)?;
|
|
||||||
state.editor.set_clip(state.pool.clip().as_ref());
|
|
||||||
undo
|
|
||||||
},
|
|
||||||
// reload clip in editor to update color
|
|
||||||
PoolCommand::Phrase(MidiPoolCommand::SetColor(index, _)) => {
|
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Phrases)?;
|
|
||||||
state.editor.set_clip(state.pool.clip().as_ref());
|
|
||||||
undo
|
|
||||||
},
|
|
||||||
_ => cmd.delegate(&mut state.pool, Self::Phrases)?
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
command!(|self: ArrangerClipCommand, state: Arranger|match self {
|
|
||||||
Self::Get(track, scene) => { todo!() },
|
|
||||||
Self::Put(track, scene, clip) => {
|
|
||||||
let old = state.scenes[scene].clips[track].clone();
|
|
||||||
state.scenes[scene].clips[track] = clip;
|
|
||||||
Some(Self::Put(track, scene, old))
|
|
||||||
},
|
|
||||||
Self::Enqueue(track, scene) => {
|
|
||||||
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
|
|
||||||
None
|
|
||||||
},
|
|
||||||
_ => None
|
|
||||||
});
|
|
||||||
|
|
||||||
//pub struct ArrangerVCursor {
|
//pub struct ArrangerVCursor {
|
||||||
//cols: Vec<(usize, usize)>,
|
//cols: Vec<(usize, usize)>,
|
||||||
|
|
@ -983,3 +735,258 @@ command!(|self: ArrangerClipCommand, state: Arranger|match self {
|
||||||
//TuiTheme::g(32).into(),
|
//TuiTheme::g(32).into(),
|
||||||
//TuiTheme::g(32).into(),
|
//TuiTheme::g(32).into(),
|
||||||
//);
|
//);
|
||||||
|
#[derive(PartialEq, Clone, Copy, Debug, Default)]
|
||||||
|
/// Represents the current user selection in the arranger
|
||||||
|
pub enum ArrangerSelection {
|
||||||
|
/// The whole mix is selected
|
||||||
|
#[default] Mix,
|
||||||
|
/// A track is selected.
|
||||||
|
Track(usize),
|
||||||
|
/// A scene is selected.
|
||||||
|
Scene(usize),
|
||||||
|
/// A clip (track × scene) is selected.
|
||||||
|
Clip(usize, usize),
|
||||||
|
}
|
||||||
|
/// Focus identification methods
|
||||||
|
impl ArrangerSelection {
|
||||||
|
pub fn is_mix (&self) -> bool { matches!(self, Self::Mix) }
|
||||||
|
pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) }
|
||||||
|
pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) }
|
||||||
|
pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) }
|
||||||
|
pub fn description (
|
||||||
|
&self,
|
||||||
|
tracks: &[ArrangerTrack],
|
||||||
|
scenes: &[ArrangerScene],
|
||||||
|
) -> Arc<str> {
|
||||||
|
format!("Selected: {}", match self {
|
||||||
|
Self::Mix => "Everything".to_string(),
|
||||||
|
Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name))
|
||||||
|
.unwrap_or_else(||"T??".into()),
|
||||||
|
Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name))
|
||||||
|
.unwrap_or_else(||"S??".into()),
|
||||||
|
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
|
||||||
|
(Some(_), Some(scene)) => match scene.clip(*t) {
|
||||||
|
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
|
||||||
|
None => format!("T{t} S{s}: Empty")
|
||||||
|
},
|
||||||
|
_ => format!("T{t} S{s}: Empty"),
|
||||||
|
}
|
||||||
|
}).into()
|
||||||
|
}
|
||||||
|
pub fn track (&self) -> Option<usize> {
|
||||||
|
use ArrangerSelection::*;
|
||||||
|
match self {
|
||||||
|
Clip(t, _) => Some(*t),
|
||||||
|
Track(t) => Some(*t),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn scene (&self) -> Option<usize> {
|
||||||
|
use ArrangerSelection::*;
|
||||||
|
match self {
|
||||||
|
Clip(_, s) => Some(*s),
|
||||||
|
Scene(s) => Some(*s),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Arranger {
|
||||||
|
pub fn track_next_name (&self) -> Arc<str> {
|
||||||
|
format!("Trk{:02}", self.tracks.len() + 1).into()
|
||||||
|
}
|
||||||
|
pub fn track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
||||||
|
-> Usually<&mut ArrangerTrack>
|
||||||
|
{
|
||||||
|
let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into());
|
||||||
|
let track = ArrangerTrack {
|
||||||
|
width: (name.len() + 2).max(9),
|
||||||
|
color: color.unwrap_or_else(ItemPalette::random),
|
||||||
|
player: MidiPlayer::from(&self.clock),
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
self.tracks.push(track);
|
||||||
|
let len = self.tracks.len();
|
||||||
|
let index = len - 1;
|
||||||
|
for scene in self.scenes.iter_mut() {
|
||||||
|
while scene.clips.len() < len {
|
||||||
|
scene.clips.push(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(&mut self.tracks[index])
|
||||||
|
}
|
||||||
|
pub fn track_del (&mut self, index: usize) {
|
||||||
|
self.tracks.remove(index);
|
||||||
|
for scene in self.scenes.iter_mut() {
|
||||||
|
scene.clips.remove(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn tracks_add (
|
||||||
|
&mut self,
|
||||||
|
count: usize,
|
||||||
|
width: usize,
|
||||||
|
midi_from: &[PortConnection],
|
||||||
|
midi_to: &[PortConnection],
|
||||||
|
) -> Usually<()> {
|
||||||
|
let jack = self.jack.clone();
|
||||||
|
let track_color_1 = ItemColor::random();
|
||||||
|
let track_color_2 = ItemColor::random();
|
||||||
|
for i in 0..count {
|
||||||
|
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
|
||||||
|
let mut track = self.track_add(None, Some(color))?;
|
||||||
|
track.width = width;
|
||||||
|
let port = JackPort::<MidiIn>::new(&jack, &format!("{}I", &track.name), midi_from)?;
|
||||||
|
track.player.midi_ins.push(port);
|
||||||
|
let port = JackPort::<MidiOut>::new(&jack, &format!("{}O", &track.name), midi_to)?;
|
||||||
|
track.player.midi_outs.push(port);
|
||||||
|
}
|
||||||
|
// TODO: port per track:
|
||||||
|
//for connection in midi_from.iter() {
|
||||||
|
//let mut split = connection.as_ref().split("=");
|
||||||
|
//let number = split.next().unwrap().trim();
|
||||||
|
//if let Ok(track) = number.parse::<usize>() {
|
||||||
|
//if track < 1 {
|
||||||
|
//panic!("Tracks start from 1")
|
||||||
|
//}
|
||||||
|
//if track > count {
|
||||||
|
//panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.")
|
||||||
|
//}
|
||||||
|
//if let Some(port) = split.next() {
|
||||||
|
//if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() {
|
||||||
|
////jack.read().unwrap().client().connect_ports(port, &self.tracks[track-1].player.midi_ins[0])?;
|
||||||
|
//} else {
|
||||||
|
//panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
|
||||||
|
//}
|
||||||
|
//} else {
|
||||||
|
//panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME")
|
||||||
|
//}
|
||||||
|
//} else {
|
||||||
|
//panic!("Failed to parse track number: {number}")
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//for connection in midi_to.iter() {
|
||||||
|
//let mut split = connection.as_ref().split("=");
|
||||||
|
//let number = split.next().unwrap().trim();
|
||||||
|
//if let Ok(track) = number.parse::<usize>() {
|
||||||
|
//if track < 1 {
|
||||||
|
//panic!("Tracks start from 1")
|
||||||
|
//}
|
||||||
|
//if track > count {
|
||||||
|
//panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.")
|
||||||
|
//}
|
||||||
|
//if let Some(port) = split.next() {
|
||||||
|
//if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() {
|
||||||
|
////jack.read().unwrap().client().connect_ports(&self.tracks[track-1].player.midi_outs[0], port)?;
|
||||||
|
//} else {
|
||||||
|
//panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
|
||||||
|
//}
|
||||||
|
//} else {
|
||||||
|
//panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME")
|
||||||
|
//}
|
||||||
|
//} else {
|
||||||
|
//panic!("Failed to parse track number: {number}")
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug)] pub struct ArrangerTrack {
|
||||||
|
/// Name of track
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Preferred width of track column
|
||||||
|
pub width: usize,
|
||||||
|
/// Identifying color of track
|
||||||
|
pub color: ItemPalette,
|
||||||
|
/// MIDI player state
|
||||||
|
pub player: MidiPlayer,
|
||||||
|
}
|
||||||
|
has_clock!(|self:ArrangerTrack|self.player.clock());
|
||||||
|
has_player!(|self:ArrangerTrack|self.player);
|
||||||
|
impl ArrangerTrack {
|
||||||
|
fn longest_name (tracks: &[Self]) -> usize {
|
||||||
|
tracks.iter().map(|s|s.name.len()).fold(0, usize::max)
|
||||||
|
}
|
||||||
|
fn width_inc (&mut self) {
|
||||||
|
self.width += 1;
|
||||||
|
}
|
||||||
|
fn width_dec (&mut self) {
|
||||||
|
if self.width > Arranger::TRACK_MIN_WIDTH {
|
||||||
|
self.width -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Arranger {
|
||||||
|
pub fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
||||||
|
-> Usually<&mut ArrangerScene>
|
||||||
|
{
|
||||||
|
let scene = ArrangerScene {
|
||||||
|
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
|
||||||
|
clips: vec![None;self.tracks.len()],
|
||||||
|
color: color.unwrap_or_else(ItemPalette::random),
|
||||||
|
};
|
||||||
|
self.scenes.push(scene);
|
||||||
|
let index = self.scenes.len() - 1;
|
||||||
|
Ok(&mut self.scenes[index])
|
||||||
|
}
|
||||||
|
pub fn scene_del (&mut self, index: usize) {
|
||||||
|
todo!("delete scene");
|
||||||
|
}
|
||||||
|
fn scene_default_name (&self) -> Arc<str> {
|
||||||
|
format!("Sc{:3>}", self.scenes.len() + 1).into()
|
||||||
|
}
|
||||||
|
pub fn selected_scene (&self) -> Option<&ArrangerScene> {
|
||||||
|
self.selected.scene().and_then(|s|self.scenes.get(s))
|
||||||
|
}
|
||||||
|
pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
|
||||||
|
self.selected.scene().and_then(|s|self.scenes.get_mut(s))
|
||||||
|
}
|
||||||
|
pub fn scenes_add (&mut self, n: usize) -> Usually<()> {
|
||||||
|
let scene_color_1 = ItemColor::random();
|
||||||
|
let scene_color_2 = ItemColor::random();
|
||||||
|
for i in 0..n {
|
||||||
|
let _scene = self.scene_add(None, Some(
|
||||||
|
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Default, Debug, Clone)] pub struct ArrangerScene {
|
||||||
|
/// Name of scene
|
||||||
|
pub(crate) name: Arc<str>,
|
||||||
|
/// Clips in scene, one per track
|
||||||
|
pub(crate) clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
||||||
|
/// Identifying color of scene
|
||||||
|
pub(crate) color: ItemPalette,
|
||||||
|
}
|
||||||
|
impl ArrangerScene {
|
||||||
|
pub fn longest_name (scenes: &[Self]) -> usize {
|
||||||
|
scenes.iter().map(|s|s.name.len()).fold(0, usize::max)
|
||||||
|
}
|
||||||
|
/// Returns the pulse length of the longest clip in the scene
|
||||||
|
pub fn pulses (&self) -> usize {
|
||||||
|
self.clips.iter().fold(0, |a, p|{
|
||||||
|
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// Returns true if all clips in the scene are
|
||||||
|
/// currently playing on the given collection of tracks.
|
||||||
|
pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> bool {
|
||||||
|
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
|
||||||
|
.all(|(track_index, clip)|match clip {
|
||||||
|
Some(c) => tracks
|
||||||
|
.get(track_index)
|
||||||
|
.map(|track|{
|
||||||
|
if let Some((_, Some(clip))) = track.player().play_clip() {
|
||||||
|
*clip.read().unwrap() == *c.read().unwrap()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(false),
|
||||||
|
None => true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
|
||||||
|
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
// TODO
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum ArrangerSceneCommand {
|
|
||||||
Add,
|
|
||||||
Delete(usize),
|
|
||||||
Swap(usize, usize),
|
|
||||||
SetSize(usize),
|
|
||||||
SetZoom(usize),
|
|
||||||
SetColor(usize, ItemPalette),
|
|
||||||
Enqueue(usize),
|
|
||||||
}
|
|
||||||
command!(|self: ArrangerSceneCommand, state: Arranger|match self {
|
|
||||||
Self::Add => {
|
|
||||||
state.scene_add(None, None)?;
|
|
||||||
None
|
|
||||||
}
|
|
||||||
Self::Delete(index) => {
|
|
||||||
state.scene_del(index);
|
|
||||||
None
|
|
||||||
},
|
|
||||||
Self::SetColor(index, color) => {
|
|
||||||
let old = state.scenes[index].color;
|
|
||||||
state.scenes[index].color = color;
|
|
||||||
Some(Self::SetColor(index, old))
|
|
||||||
},
|
|
||||||
Self::Enqueue(scene) => {
|
|
||||||
for track in 0..state.tracks.len() {
|
|
||||||
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
|
|
||||||
}
|
|
||||||
None
|
|
||||||
},
|
|
||||||
_ => None
|
|
||||||
});
|
|
||||||
impl Arranger {
|
|
||||||
pub fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
|
||||||
-> Usually<&mut ArrangerScene>
|
|
||||||
{
|
|
||||||
let scene = ArrangerScene {
|
|
||||||
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
|
|
||||||
clips: vec![None;self.tracks.len()],
|
|
||||||
color: color.unwrap_or_else(ItemPalette::random),
|
|
||||||
};
|
|
||||||
self.scenes.push(scene);
|
|
||||||
let index = self.scenes.len() - 1;
|
|
||||||
Ok(&mut self.scenes[index])
|
|
||||||
}
|
|
||||||
pub fn scene_del (&mut self, index: usize) {
|
|
||||||
todo!("delete scene");
|
|
||||||
}
|
|
||||||
fn scene_default_name (&self) -> Arc<str> {
|
|
||||||
format!("Sc{:3>}", self.scenes.len() + 1).into()
|
|
||||||
}
|
|
||||||
pub fn selected_scene (&self) -> Option<&ArrangerScene> {
|
|
||||||
self.selected.scene().and_then(|s|self.scenes.get(s))
|
|
||||||
}
|
|
||||||
pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
|
|
||||||
self.selected.scene().and_then(|s|self.scenes.get_mut(s))
|
|
||||||
}
|
|
||||||
pub fn scenes_add (&mut self, n: usize) -> Usually<()> {
|
|
||||||
let scene_color_1 = ItemColor::random();
|
|
||||||
let scene_color_2 = ItemColor::random();
|
|
||||||
for i in 0..n {
|
|
||||||
let _scene = self.scene_add(None, Some(
|
|
||||||
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Default, Debug, Clone)] pub struct ArrangerScene {
|
|
||||||
/// Name of scene
|
|
||||||
pub(crate) name: Arc<str>,
|
|
||||||
/// Clips in scene, one per track
|
|
||||||
pub(crate) clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
|
||||||
/// Identifying color of scene
|
|
||||||
pub(crate) color: ItemPalette,
|
|
||||||
}
|
|
||||||
impl ArrangerScene {
|
|
||||||
pub fn name (&self) -> &Arc<str> {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
pub fn clips (&self) -> &Vec<Option<Arc<RwLock<MidiClip>>>> {
|
|
||||||
&self.clips
|
|
||||||
}
|
|
||||||
pub fn color (&self) -> ItemPalette {
|
|
||||||
self.color
|
|
||||||
}
|
|
||||||
pub fn longest_name (scenes: &[Self]) -> usize {
|
|
||||||
scenes.iter().map(|s|s.name.len()).fold(0, usize::max)
|
|
||||||
}
|
|
||||||
/// Returns the pulse length of the longest clip in the scene
|
|
||||||
pub fn pulses (&self) -> usize {
|
|
||||||
self.clips().iter().fold(0, |a, p|{
|
|
||||||
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/// Returns true if all clips in the scene are
|
|
||||||
/// currently playing on the given collection of tracks.
|
|
||||||
pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> bool {
|
|
||||||
self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate()
|
|
||||||
.all(|(track_index, clip)|match clip {
|
|
||||||
Some(c) => tracks
|
|
||||||
.get(track_index)
|
|
||||||
.map(|track|{
|
|
||||||
if let Some((_, Some(clip))) = track.player().play_clip() {
|
|
||||||
*clip.read().unwrap() == *c.read().unwrap()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or(false),
|
|
||||||
None => true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
|
|
||||||
match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
#[derive(PartialEq, Clone, Copy, Debug, Default)]
|
|
||||||
/// Represents the current user selection in the arranger
|
|
||||||
pub enum ArrangerSelection {
|
|
||||||
/// The whole mix is selected
|
|
||||||
#[default] Mix,
|
|
||||||
/// A track is selected.
|
|
||||||
Track(usize),
|
|
||||||
/// A scene is selected.
|
|
||||||
Scene(usize),
|
|
||||||
/// A clip (track × scene) is selected.
|
|
||||||
Clip(usize, usize),
|
|
||||||
}
|
|
||||||
/// Focus identification methods
|
|
||||||
impl ArrangerSelection {
|
|
||||||
pub fn is_mix (&self) -> bool { matches!(self, Self::Mix) }
|
|
||||||
pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) }
|
|
||||||
pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) }
|
|
||||||
pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) }
|
|
||||||
pub fn description (
|
|
||||||
&self,
|
|
||||||
tracks: &[ArrangerTrack],
|
|
||||||
scenes: &[ArrangerScene],
|
|
||||||
) -> Arc<str> {
|
|
||||||
format!("Selected: {}", match self {
|
|
||||||
Self::Mix => "Everything".to_string(),
|
|
||||||
Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name))
|
|
||||||
.unwrap_or_else(||"T??".into()),
|
|
||||||
Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name))
|
|
||||||
.unwrap_or_else(||"S??".into()),
|
|
||||||
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
|
|
||||||
(Some(_), Some(scene)) => match scene.clip(*t) {
|
|
||||||
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
|
|
||||||
None => format!("T{t} S{s}: Empty")
|
|
||||||
},
|
|
||||||
_ => format!("T{t} S{s}: Empty"),
|
|
||||||
}
|
|
||||||
}).into()
|
|
||||||
}
|
|
||||||
pub fn track (&self) -> Option<usize> {
|
|
||||||
use ArrangerSelection::*;
|
|
||||||
match self {
|
|
||||||
Clip(t, _) => Some(*t),
|
|
||||||
Track(t) => Some(*t),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn scene (&self) -> Option<usize> {
|
|
||||||
use ArrangerSelection::*;
|
|
||||||
match self {
|
|
||||||
Clip(_, s) => Some(*s),
|
|
||||||
Scene(s) => Some(*s),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,189 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum ArrangerTrackCommand {
|
|
||||||
Add,
|
|
||||||
Delete(usize),
|
|
||||||
Stop(usize),
|
|
||||||
Swap(usize, usize),
|
|
||||||
SetSize(usize),
|
|
||||||
SetZoom(usize),
|
|
||||||
SetColor(usize, ItemPalette),
|
|
||||||
}
|
|
||||||
command!(|self: ArrangerTrackCommand, state: Arranger|match self {
|
|
||||||
Self::Add => {
|
|
||||||
state.track_add(None, None)?;
|
|
||||||
None
|
|
||||||
},
|
|
||||||
Self::SetColor(index, color) => {
|
|
||||||
let old = state.tracks[index].color;
|
|
||||||
state.tracks[index].color = color;
|
|
||||||
Some(Self::SetColor(index, old))
|
|
||||||
},
|
|
||||||
Self::Stop(track) => {
|
|
||||||
state.tracks[track].player.enqueue_next(None);
|
|
||||||
None
|
|
||||||
},
|
|
||||||
_ => None
|
|
||||||
});
|
|
||||||
impl Arranger {
|
|
||||||
pub fn track_next_name (&self) -> Arc<str> {
|
|
||||||
format!("Trk{:02}", self.tracks.len() + 1).into()
|
|
||||||
}
|
|
||||||
pub fn track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
|
||||||
-> Usually<&mut ArrangerTrack>
|
|
||||||
{
|
|
||||||
let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into());
|
|
||||||
let track = ArrangerTrack {
|
|
||||||
width: (name.len() + 2).max(9),
|
|
||||||
color: color.unwrap_or_else(ItemPalette::random),
|
|
||||||
player: MidiPlayer::from(&self.clock),
|
|
||||||
name,
|
|
||||||
};
|
|
||||||
self.tracks.push(track);
|
|
||||||
let len = self.tracks.len();
|
|
||||||
let index = len - 1;
|
|
||||||
for scene in self.scenes.iter_mut() {
|
|
||||||
while scene.clips.len() < len {
|
|
||||||
scene.clips.push(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(&mut self.tracks[index])
|
|
||||||
}
|
|
||||||
pub fn track_del (&mut self, index: usize) {
|
|
||||||
self.tracks.remove(index);
|
|
||||||
for scene in self.scenes.iter_mut() {
|
|
||||||
scene.clips.remove(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn tracks_add (
|
|
||||||
&mut self,
|
|
||||||
count: usize,
|
|
||||||
width: usize,
|
|
||||||
midi_from: &[PortConnection],
|
|
||||||
midi_to: &[PortConnection],
|
|
||||||
) -> Usually<()> {
|
|
||||||
let jack = self.jack.clone();
|
|
||||||
let track_color_1 = ItemColor::random();
|
|
||||||
let track_color_2 = ItemColor::random();
|
|
||||||
for i in 0..count {
|
|
||||||
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
|
|
||||||
let mut track = self.track_add(None, Some(color))?;
|
|
||||||
track.width = width;
|
|
||||||
let port = JackPort::<MidiIn>::new(&jack, &format!("{}I", &track.name), midi_from)?;
|
|
||||||
track.player.midi_ins.push(port);
|
|
||||||
let port = JackPort::<MidiOut>::new(&jack, &format!("{}O", &track.name), midi_to)?;
|
|
||||||
track.player.midi_outs.push(port);
|
|
||||||
}
|
|
||||||
// TODO: port per track:
|
|
||||||
//for connection in midi_from.iter() {
|
|
||||||
//let mut split = connection.as_ref().split("=");
|
|
||||||
//let number = split.next().unwrap().trim();
|
|
||||||
//if let Ok(track) = number.parse::<usize>() {
|
|
||||||
//if track < 1 {
|
|
||||||
//panic!("Tracks start from 1")
|
|
||||||
//}
|
|
||||||
//if track > count {
|
|
||||||
//panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.")
|
|
||||||
//}
|
|
||||||
//if let Some(port) = split.next() {
|
|
||||||
//if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() {
|
|
||||||
////jack.read().unwrap().client().connect_ports(port, &self.tracks[track-1].player.midi_ins[0])?;
|
|
||||||
//} else {
|
|
||||||
//panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
|
|
||||||
//}
|
|
||||||
//} else {
|
|
||||||
//panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME")
|
|
||||||
//}
|
|
||||||
//} else {
|
|
||||||
//panic!("Failed to parse track number: {number}")
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//for connection in midi_to.iter() {
|
|
||||||
//let mut split = connection.as_ref().split("=");
|
|
||||||
//let number = split.next().unwrap().trim();
|
|
||||||
//if let Ok(track) = number.parse::<usize>() {
|
|
||||||
//if track < 1 {
|
|
||||||
//panic!("Tracks start from 1")
|
|
||||||
//}
|
|
||||||
//if track > count {
|
|
||||||
//panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.")
|
|
||||||
//}
|
|
||||||
//if let Some(port) = split.next() {
|
|
||||||
//if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() {
|
|
||||||
////jack.read().unwrap().client().connect_ports(&self.tracks[track-1].player.midi_outs[0], port)?;
|
|
||||||
//} else {
|
|
||||||
//panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
|
|
||||||
//}
|
|
||||||
//} else {
|
|
||||||
//panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME")
|
|
||||||
//}
|
|
||||||
//} else {
|
|
||||||
//panic!("Failed to parse track number: {number}")
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Debug)] pub struct ArrangerTrack {
|
|
||||||
/// Name of track
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Preferred width of track column
|
|
||||||
pub width: usize,
|
|
||||||
/// Identifying color of track
|
|
||||||
pub color: ItemPalette,
|
|
||||||
/// MIDI player state
|
|
||||||
pub player: MidiPlayer,
|
|
||||||
}
|
|
||||||
has_clock!(|self:ArrangerTrack|self.player.clock());
|
|
||||||
has_player!(|self:ArrangerTrack|self.player);
|
|
||||||
impl ArrangerTrack {
|
|
||||||
/// Name of track
|
|
||||||
pub fn name (&self) -> &Arc<str> {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
/// Preferred width of track column
|
|
||||||
fn width (&self) -> usize {
|
|
||||||
self.width
|
|
||||||
}
|
|
||||||
/// Preferred width of track column
|
|
||||||
fn width_mut (&mut self) -> &mut usize {
|
|
||||||
&mut self.width
|
|
||||||
}
|
|
||||||
/// Identifying color of track
|
|
||||||
pub fn color (&self) -> ItemPalette {
|
|
||||||
self.color
|
|
||||||
}
|
|
||||||
fn longest_name (tracks: &[Self]) -> usize {
|
|
||||||
tracks.iter().map(|s|s.name.len()).fold(0, usize::max)
|
|
||||||
}
|
|
||||||
fn width_inc (&mut self) {
|
|
||||||
*self.width_mut() += 1;
|
|
||||||
}
|
|
||||||
fn width_dec (&mut self) {
|
|
||||||
if self.width() > Arranger::TRACK_MIN_WIDTH {
|
|
||||||
*self.width_mut() -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Hosts the JACK callback for a collection of tracks
|
|
||||||
pub struct TracksAudio<'a>(
|
|
||||||
// Track collection
|
|
||||||
pub &'a mut [ArrangerTrack],
|
|
||||||
/// Note buffer
|
|
||||||
pub &'a mut Vec<u8>,
|
|
||||||
/// Note chunk buffer
|
|
||||||
pub &'a mut Vec<Vec<Vec<u8>>>,
|
|
||||||
);
|
|
||||||
impl Audio for TracksAudio<'_> {
|
|
||||||
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
|
||||||
let model = &mut self.0;
|
|
||||||
let note_buffer = &mut self.1;
|
|
||||||
let output_buffer = &mut self.2;
|
|
||||||
for track in model.iter_mut() {
|
|
||||||
if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit {
|
|
||||||
return Control::Quit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
132
tek/src/audio.rs
Normal file
132
tek/src/audio.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
audio!(|self: Sequencer, 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
|
||||||
|
});
|
||||||
|
|
||||||
|
audio!(|self: Groovebox, client, scope|{
|
||||||
|
// Start profiling cycle
|
||||||
|
let t0 = self.perf.get_t0();
|
||||||
|
|
||||||
|
// Update transport clock
|
||||||
|
if Control::Quit == ClockAudio(&mut self.player).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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update sampler
|
||||||
|
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].port.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End profiling cycle
|
||||||
|
self.perf.update(t0, scope);
|
||||||
|
|
||||||
|
Control::Continue
|
||||||
|
});
|
||||||
|
|
||||||
|
audio!(|self: Arranger, 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 sequencers
|
||||||
|
//let tracks = &mut self.tracks;
|
||||||
|
//let note_buf = &mut self.note_buf;
|
||||||
|
//let midi_buf = &mut self.midi_buf;
|
||||||
|
//if Control::Quit == TracksAudio(tracks, note_buf, midi_buf).process(client, scope) {
|
||||||
|
//return Control::Quit
|
||||||
|
//}
|
||||||
|
|
||||||
|
// FIXME: one of these per playing track
|
||||||
|
//self.now.set(0.);
|
||||||
|
//if let ArrangerSelection::Clip(t, s) = self.selected {
|
||||||
|
//let clip = self.scenes.get(s).map(|scene|scene.clips.get(t));
|
||||||
|
//if let Some(Some(Some(clip))) = clip {
|
||||||
|
//if let Some(track) = self.tracks().get(t) {
|
||||||
|
//if let Some((ref started_at, Some(ref playing))) = track.player.play_clip {
|
||||||
|
//let clip = clip.read().unwrap();
|
||||||
|
//if *playing.read().unwrap() == *clip {
|
||||||
|
//let pulse = self.current().pulse.get();
|
||||||
|
//let start = started_at.pulse.get();
|
||||||
|
//let now = (pulse - start) % clip.length as f64;
|
||||||
|
//self.now.set(now);
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
// End profiling cycle
|
||||||
|
self.perf.update(t0, scope);
|
||||||
|
return Control::Continue
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Hosts the JACK callback for a collection of tracks
|
||||||
|
pub struct TracksAudio<'a>(
|
||||||
|
// Track collection
|
||||||
|
pub &'a mut [ArrangerTrack],
|
||||||
|
/// Note buffer
|
||||||
|
pub &'a mut Vec<u8>,
|
||||||
|
/// Note chunk buffer
|
||||||
|
pub &'a mut Vec<Vec<Vec<u8>>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Audio for TracksAudio<'_> {
|
||||||
|
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||||
|
let model = &mut self.0;
|
||||||
|
let note_buffer = &mut self.1;
|
||||||
|
let output_buffer = &mut self.2;
|
||||||
|
for track in model.iter_mut() {
|
||||||
|
if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit {
|
||||||
|
return Control::Quit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
450
tek/src/control.rs
Normal file
450
tek/src/control.rs
Normal file
|
|
@ -0,0 +1,450 @@
|
||||||
|
use crate::*;
|
||||||
|
use ClockCommand::{Play, Pause};
|
||||||
|
use KeyCode::{Tab, Char};
|
||||||
|
use SequencerCommand as SeqCmd;
|
||||||
|
use GrooveboxCommand as GrvCmd;
|
||||||
|
use ArrangerCommand as ArrCmd;
|
||||||
|
use MidiEditCommand as EditCmd;
|
||||||
|
use MidiPoolCommand as PoolCmd;
|
||||||
|
|
||||||
|
handle!(TuiIn: |self: App, input| Ok(None));
|
||||||
|
handle!(TuiIn: |self: Sequencer, input|SequencerCommand::execute_with_state(self, input.event()));
|
||||||
|
handle!(TuiIn: |self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input.event()));
|
||||||
|
handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event()));
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)] pub enum AppCommand {
|
||||||
|
Clear,
|
||||||
|
Clip(ArrangerClipCommand),
|
||||||
|
Clock(ClockCommand),
|
||||||
|
Color(ItemPalette),
|
||||||
|
Compact(bool),
|
||||||
|
Editor(MidiEditCommand),
|
||||||
|
Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
||||||
|
History(isize),
|
||||||
|
Pool(PoolCommand),
|
||||||
|
Sampler(SamplerCommand),
|
||||||
|
Scene(ArrangerSceneCommand),
|
||||||
|
Select(ArrangerSelection),
|
||||||
|
StopAll,
|
||||||
|
Track(ArrangerTrackCommand),
|
||||||
|
Zoom(usize),
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)] pub enum SequencerCommand {
|
||||||
|
Compact(bool),
|
||||||
|
History(isize),
|
||||||
|
Clock(ClockCommand),
|
||||||
|
Pool(PoolCommand),
|
||||||
|
Editor(MidiEditCommand),
|
||||||
|
Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)] pub enum GrooveboxCommand {
|
||||||
|
Compact(bool),
|
||||||
|
History(isize),
|
||||||
|
Clock(ClockCommand),
|
||||||
|
Pool(PoolCommand),
|
||||||
|
Editor(MidiEditCommand),
|
||||||
|
Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
||||||
|
Sampler(SamplerCommand),
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)] pub enum ArrangerCommand {
|
||||||
|
History(isize),
|
||||||
|
Color(ItemPalette),
|
||||||
|
Clock(ClockCommand),
|
||||||
|
Scene(ArrangerSceneCommand),
|
||||||
|
Track(ArrangerTrackCommand),
|
||||||
|
Clip(ArrangerClipCommand),
|
||||||
|
Select(ArrangerSelection),
|
||||||
|
Zoom(usize),
|
||||||
|
Pool(PoolCommand),
|
||||||
|
Editor(MidiEditCommand),
|
||||||
|
StopAll,
|
||||||
|
Clear,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)] pub enum ArrangerClipCommand {
|
||||||
|
Get(usize, usize),
|
||||||
|
Put(usize, usize, Option<Arc<RwLock<MidiClip>>>),
|
||||||
|
Enqueue(usize, usize),
|
||||||
|
Edit(Option<Arc<RwLock<MidiClip>>>),
|
||||||
|
SetLoop(usize, usize, bool),
|
||||||
|
SetColor(usize, usize, ItemPalette),
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)] pub enum ArrangerSceneCommand {
|
||||||
|
Add,
|
||||||
|
Delete(usize),
|
||||||
|
Swap(usize, usize),
|
||||||
|
SetSize(usize),
|
||||||
|
SetZoom(usize),
|
||||||
|
SetColor(usize, ItemPalette),
|
||||||
|
Enqueue(usize),
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)] pub enum ArrangerTrackCommand {
|
||||||
|
Add,
|
||||||
|
Delete(usize),
|
||||||
|
Stop(usize),
|
||||||
|
Swap(usize, usize),
|
||||||
|
SetSize(usize),
|
||||||
|
SetZoom(usize),
|
||||||
|
SetColor(usize, ItemPalette),
|
||||||
|
}
|
||||||
|
|
||||||
|
command!(|self: SequencerCommand, state: Sequencer|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(state.pool.clip().as_ref());
|
||||||
|
undo
|
||||||
|
},
|
||||||
|
// update color in all places simultaneously
|
||||||
|
PoolCommand::Phrase(PoolCmd::SetColor(index, _)) => {
|
||||||
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
|
state.editor.set_clip(state.pool.clip().as_ref());
|
||||||
|
undo
|
||||||
|
},
|
||||||
|
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
||||||
|
},
|
||||||
|
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
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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(state.pool.clip().as_ref());
|
||||||
|
undo
|
||||||
|
},
|
||||||
|
// update color in all places simultaneously
|
||||||
|
PoolCommand::Phrase(PoolCmd::SetColor(index, _)) => {
|
||||||
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
|
state.editor.set_clip(state.pool.clip().as_ref());
|
||||||
|
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
|
||||||
|
},
|
||||||
|
});
|
||||||
|
command!(|self: ArrangerCommand, state: Arranger|match self {
|
||||||
|
Self::Clear => { todo!() },
|
||||||
|
Self::History(_) => { todo!() },
|
||||||
|
Self::Zoom(_) => { todo!(); },
|
||||||
|
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
||||||
|
Self::Clip(cmd) => cmd.delegate(state, Self::Clip)?,
|
||||||
|
Self::Scene(cmd) => cmd.delegate(state, Self::Scene)?,
|
||||||
|
Self::Track(cmd) => cmd.delegate(state, Self::Track)?,
|
||||||
|
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
||||||
|
Self::Select(selected) => { state.selected = selected; None },
|
||||||
|
Self::StopAll => {
|
||||||
|
for track in 0..state.tracks.len() { state.tracks[track].player.enqueue_next(None); }
|
||||||
|
None
|
||||||
|
},
|
||||||
|
Self::Color(palette) => {
|
||||||
|
let old = state.color;
|
||||||
|
state.color = palette;
|
||||||
|
Some(Self::Color(old))
|
||||||
|
},
|
||||||
|
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(state.pool.clip().as_ref());
|
||||||
|
undo
|
||||||
|
},
|
||||||
|
// reload clip in editor to update color
|
||||||
|
PoolCommand::Phrase(MidiPoolCommand::SetColor(index, _)) => {
|
||||||
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
|
state.editor.set_clip(state.pool.clip().as_ref());
|
||||||
|
undo
|
||||||
|
},
|
||||||
|
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
command!(|self: ArrangerSceneCommand, state: Arranger|match self {
|
||||||
|
Self::Add => {
|
||||||
|
state.scene_add(None, None)?;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Self::Delete(index) => {
|
||||||
|
state.scene_del(index);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
Self::SetColor(index, color) => {
|
||||||
|
let old = state.scenes[index].color;
|
||||||
|
state.scenes[index].color = color;
|
||||||
|
Some(Self::SetColor(index, old))
|
||||||
|
},
|
||||||
|
Self::Enqueue(scene) => {
|
||||||
|
for track in 0..state.tracks.len() {
|
||||||
|
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
},
|
||||||
|
_ => None
|
||||||
|
});
|
||||||
|
command!(|self: ArrangerTrackCommand, state: Arranger|match self {
|
||||||
|
Self::Add => {
|
||||||
|
state.track_add(None, None)?;
|
||||||
|
None
|
||||||
|
},
|
||||||
|
Self::SetColor(index, color) => {
|
||||||
|
let old = state.tracks[index].color;
|
||||||
|
state.tracks[index].color = color;
|
||||||
|
Some(Self::SetColor(index, old))
|
||||||
|
},
|
||||||
|
Self::Stop(track) => {
|
||||||
|
state.tracks[track].player.enqueue_next(None);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
_ => None
|
||||||
|
});
|
||||||
|
command!(|self: ArrangerClipCommand, state: Arranger|match self {
|
||||||
|
Self::Get(track, scene) => { todo!() },
|
||||||
|
Self::Put(track, scene, clip) => {
|
||||||
|
let old = state.scenes[scene].clips[track].clone();
|
||||||
|
state.scenes[scene].clips[track] = clip;
|
||||||
|
Some(Self::Put(track, scene, old))
|
||||||
|
},
|
||||||
|
Self::Enqueue(track, scene) => {
|
||||||
|
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
|
||||||
|
None
|
||||||
|
},
|
||||||
|
_ => None
|
||||||
|
});
|
||||||
|
keymap!(KEYS_SEQUENCER = |state: Sequencer, input: Event| SequencerCommand {
|
||||||
|
// TODO: k: toggle on-screen keyboard
|
||||||
|
ctrl(key(Char('k'))) => { todo!("keyboard") },
|
||||||
|
// Transport: Play/pause
|
||||||
|
key(Char(' ')) => SeqCmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
|
||||||
|
// Transport: Play from start or rewind to start
|
||||||
|
shift(key(Char(' '))) => SeqCmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
|
||||||
|
// u: undo
|
||||||
|
key(Char('u')) => SeqCmd::History(-1),
|
||||||
|
// Shift-U: redo
|
||||||
|
key(Char('U')) => SeqCmd::History( 1),
|
||||||
|
// Tab: Toggle compact mode
|
||||||
|
key(Tab) => SeqCmd::Compact(!state.compact),
|
||||||
|
// q: Enqueue currently edited clip
|
||||||
|
key(Char('q')) => SeqCmd::Enqueue(state.pool.clip().clone()),
|
||||||
|
// 0: Enqueue clip 0 (stop all)
|
||||||
|
key(Char('0')) => SeqCmd::Enqueue(Some(state.clips()[0].clone())),
|
||||||
|
// e: Toggle between editing currently playing or other clip
|
||||||
|
//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();
|
||||||
|
//SeqCmd::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) {
|
||||||
|
SeqCmd::Editor(command)
|
||||||
|
} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
|
||||||
|
SeqCmd::Pool(command)
|
||||||
|
} else {
|
||||||
|
return None
|
||||||
|
});
|
||||||
|
keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand {
|
||||||
|
// Tab: Toggle compact mode
|
||||||
|
key(Tab) => GrvCmd::Compact(!state.compact),
|
||||||
|
// q: Enqueue currently edited clip
|
||||||
|
key(Char('q')) => GrvCmd::Enqueue(state.pool.clip().clone()),
|
||||||
|
// 0: Enqueue clip 0 (stop all)
|
||||||
|
key(Char('0')) => GrvCmd::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(' '))) => GrvCmd::Clock(
|
||||||
|
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
|
||||||
|
),
|
||||||
|
// Shift-R: toggle recording
|
||||||
|
shift(key(Char('R'))) => GrvCmd::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)) => GrvCmd::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().map(|s|s.read().unwrap().clone());
|
||||||
|
//GrvCmd::Editor(Show(if selected != editing {
|
||||||
|
//selected
|
||||||
|
//} else {
|
||||||
|
//Some(playing.clone())
|
||||||
|
//}))
|
||||||
|
//} else {
|
||||||
|
//return None
|
||||||
|
//},
|
||||||
|
}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) {
|
||||||
|
GrvCmd::Editor(command)
|
||||||
|
} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
|
||||||
|
GrvCmd::Pool(command)
|
||||||
|
} else {
|
||||||
|
return None
|
||||||
|
});
|
||||||
|
keymap!(KEYS_ARRANGER = |state: Arranger, input: Event| ArrangerCommand {
|
||||||
|
key(Char('u')) => ArrCmd::History(-1),
|
||||||
|
key(Char('U')) => ArrCmd::History(1),
|
||||||
|
// TODO: k: toggle on-screen keyboard
|
||||||
|
ctrl(key(Char('k'))) => { todo!("keyboard") },
|
||||||
|
// Transport: Play/pause
|
||||||
|
key(Char(' ')) => ArrCmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
|
||||||
|
// Transport: Play from start or rewind to start
|
||||||
|
shift(key(Char(' '))) => ArrCmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
|
||||||
|
key(Char('e')) => ArrCmd::Editor(MidiEditCommand::Show(state.pool.clip().clone())),
|
||||||
|
ctrl(key(Char('a'))) => ArrCmd::Scene(ArrangerSceneCommand::Add),
|
||||||
|
ctrl(key(Char('A'))) => return None,//ArrCmd::Scene(ArrangerSceneCommand::Add),
|
||||||
|
ctrl(key(Char('t'))) => ArrCmd::Track(ArrangerTrackCommand::Add),
|
||||||
|
// Tab: Toggle visibility of clip pool column
|
||||||
|
key(Tab) => ArrCmd::Pool(PoolCommand::Show(!state.pool.visible)),
|
||||||
|
}, {
|
||||||
|
use ArrangerSelection as Selected;
|
||||||
|
use ArrangerSceneCommand as Scene;
|
||||||
|
use ArrangerTrackCommand as Track;
|
||||||
|
use ArrangerClipCommand as Clip;
|
||||||
|
let t_len = state.tracks.len();
|
||||||
|
let s_len = state.scenes.len();
|
||||||
|
match state.selected {
|
||||||
|
Selected::Clip(t, s) => clip_keymap(state, input, t, s),
|
||||||
|
Selected::Scene(s) => scene_keymap(state, input, s),
|
||||||
|
Selected::Track(t) => track_keymap(state, input, t),
|
||||||
|
Selected::Mix => match input {
|
||||||
|
|
||||||
|
kpat!(Delete) => Some(ArrCmd::Clear),
|
||||||
|
kpat!(Char('0')) => Some(ArrCmd::StopAll),
|
||||||
|
kpat!(Char('c')) => Some(ArrCmd::Color(ItemPalette::random())),
|
||||||
|
|
||||||
|
kpat!(Up) => return None,
|
||||||
|
kpat!(Down) => Some( ArrCmd::Select(Selected::Scene(0))),
|
||||||
|
kpat!(Left) => return None,
|
||||||
|
kpat!(Right) => Some( ArrCmd::Select(Selected::Track(0))),
|
||||||
|
|
||||||
|
_ => None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}.or_else(||if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) {
|
||||||
|
Some(ArrCmd::Editor(command))
|
||||||
|
} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
|
||||||
|
Some(ArrCmd::Pool(command))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})?);
|
||||||
|
|
||||||
|
fn clip_keymap (state: &Arranger, input: &Event, t: usize, s: usize) -> Option<ArrangerCommand> {
|
||||||
|
use ArrangerSelection as Selected;
|
||||||
|
use ArrangerSceneCommand as Scene;
|
||||||
|
use ArrangerTrackCommand as Track;
|
||||||
|
use ArrangerClipCommand as Clip;
|
||||||
|
let t_len = state.tracks.len();
|
||||||
|
let s_len = state.scenes.len();
|
||||||
|
Some(match input {
|
||||||
|
|
||||||
|
kpat!(Char('g')) => ArrCmd::Pool(PoolCommand::Select(0)),
|
||||||
|
kpat!(Char('q')) => ArrCmd::Clip(Clip::Enqueue(t, s)),
|
||||||
|
kpat!(Char('l')) => ArrCmd::Clip(Clip::SetLoop(t, s, false)),
|
||||||
|
|
||||||
|
kpat!(Enter) => if state.scenes[s].clips[t].is_none() {
|
||||||
|
// FIXME: get this clip from the pool (autoregister via intmut)
|
||||||
|
let (_, clip) = state.add_clip();
|
||||||
|
ArrCmd::Clip(Clip::Put(t, s, Some(clip)))
|
||||||
|
} else {
|
||||||
|
return None
|
||||||
|
},
|
||||||
|
kpat!(Delete) => ArrCmd::Clip(Clip::Put(t, s, None)),
|
||||||
|
kpat!(Char('p')) => ArrCmd::Clip(Clip::Put(t, s, state.pool.clip().clone())),
|
||||||
|
kpat!(Char(',')) => ArrCmd::Clip(Clip::Put(t, s, None)),
|
||||||
|
kpat!(Char('.')) => ArrCmd::Clip(Clip::Put(t, s, None)),
|
||||||
|
kpat!(Char('<')) => ArrCmd::Clip(Clip::Put(t, s, None)),
|
||||||
|
kpat!(Char('>')) => ArrCmd::Clip(Clip::Put(t, s, None)),
|
||||||
|
|
||||||
|
kpat!(Up) => ArrCmd::Select(if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) }),
|
||||||
|
kpat!(Down) => ArrCmd::Select(Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1)))),
|
||||||
|
kpat!(Left) => ArrCmd::Select(if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) }),
|
||||||
|
kpat!(Right) => ArrCmd::Select(Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s)),
|
||||||
|
|
||||||
|
_ => return None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn scene_keymap (state: &Arranger, input: &Event, s: usize) -> Option<ArrangerCommand> {
|
||||||
|
use ArrangerSelection as Selected;
|
||||||
|
use ArrangerSceneCommand as Scene;
|
||||||
|
use ArrangerTrackCommand as Track;
|
||||||
|
use ArrangerClipCommand as Clip;
|
||||||
|
let t_len = state.tracks.len();
|
||||||
|
let s_len = state.scenes.len();
|
||||||
|
Some(match input {
|
||||||
|
|
||||||
|
kpat!(Char(',')) => ArrCmd::Scene(Scene::Swap(s, s - 1)),
|
||||||
|
kpat!(Char('.')) => ArrCmd::Scene(Scene::Swap(s, s + 1)),
|
||||||
|
kpat!(Char('<')) => ArrCmd::Scene(Scene::Swap(s, s - 1)),
|
||||||
|
kpat!(Char('>')) => ArrCmd::Scene(Scene::Swap(s, s + 1)),
|
||||||
|
kpat!(Char('q')) => ArrCmd::Scene(Scene::Enqueue(s)),
|
||||||
|
kpat!(Delete) => ArrCmd::Scene(Scene::Delete(s)),
|
||||||
|
kpat!(Char('c')) => ArrCmd::Scene(Scene::SetColor(s, ItemPalette::random())),
|
||||||
|
|
||||||
|
kpat!(Up) => ArrCmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix }),
|
||||||
|
kpat!(Down) => ArrCmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1)))),
|
||||||
|
kpat!(Left) => return None,
|
||||||
|
kpat!(Right) => ArrCmd::Select(Selected::Clip(0, s)),
|
||||||
|
|
||||||
|
_ => return None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn track_keymap (state: &Arranger, input: &Event, t: usize) -> Option<ArrangerCommand> {
|
||||||
|
use ArrangerSelection as Selected;
|
||||||
|
use ArrangerSceneCommand as Scene;
|
||||||
|
use ArrangerTrackCommand as Track;
|
||||||
|
use ArrangerClipCommand as Clip;
|
||||||
|
let t_len = state.tracks.len();
|
||||||
|
let s_len = state.scenes.len();
|
||||||
|
Some(match input {
|
||||||
|
|
||||||
|
kpat!(Char(',')) => ArrCmd::Track(Track::Swap(t, t - 1)),
|
||||||
|
kpat!(Char('.')) => ArrCmd::Track(Track::Swap(t, t + 1)),
|
||||||
|
kpat!(Char('<')) => ArrCmd::Track(Track::Swap(t, t - 1)),
|
||||||
|
kpat!(Char('>')) => ArrCmd::Track(Track::Swap(t, t + 1)),
|
||||||
|
kpat!(Delete) => ArrCmd::Track(Track::Delete(t)),
|
||||||
|
kpat!(Char('c')) => ArrCmd::Track(Track::SetColor(t, ItemPalette::random())),
|
||||||
|
|
||||||
|
kpat!(Up) => return None,
|
||||||
|
kpat!(Down) => ArrCmd::Select(Selected::Clip(t, 0)),
|
||||||
|
kpat!(Left) => ArrCmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix }),
|
||||||
|
kpat!(Right) => ArrCmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1)))),
|
||||||
|
|
||||||
|
_ => return None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -89,122 +89,7 @@ impl Groovebox {
|
||||||
Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(self.sampler.list(self.compact, &self.editor))))
|
Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(self.sampler.list(self.compact, &self.editor))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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].port.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
|
|
||||||
});
|
|
||||||
has_clock!(|self: Groovebox|self.player.clock());
|
has_clock!(|self: Groovebox|self.player.clock());
|
||||||
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(state.pool.clip().as_ref());
|
|
||||||
undo
|
|
||||||
},
|
|
||||||
// update color in all places simultaneously
|
|
||||||
PoolCommand::Phrase(SetColor(index, _)) => {
|
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
|
||||||
state.editor.set_clip(state.pool.clip().as_ref());
|
|
||||||
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(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().map(|s|s.read().unwrap().clone());
|
|
||||||
//Cmd::Editor(Show(if selected != editing {
|
|
||||||
//selected
|
|
||||||
//} else {
|
|
||||||
//Some(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
|
///// Status bar for sequencer app
|
||||||
//#[derive(Clone)]
|
//#[derive(Clone)]
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ pub type Usually<T> = std::result::Result<T, Box<dyn Error>>;
|
||||||
pub type Perhaps<T> = std::result::Result<Option<T>, Box<dyn Error>>;
|
pub type Perhaps<T> = std::result::Result<Option<T>, Box<dyn Error>>;
|
||||||
|
|
||||||
pub mod app; pub use self::app::*;
|
pub mod app; pub use self::app::*;
|
||||||
|
pub mod view; pub use self::view::*;
|
||||||
|
pub mod control; pub use self::control::*;
|
||||||
|
pub mod audio; pub use self::audio::*;
|
||||||
|
|
||||||
pub mod arranger; pub use self::arranger::*;
|
pub mod arranger; pub use self::arranger::*;
|
||||||
pub mod groovebox; pub use self::groovebox::*;
|
pub mod groovebox; pub use self::groovebox::*;
|
||||||
pub mod mixer; pub use self::mixer::*;
|
pub mod mixer; pub use self::mixer::*;
|
||||||
|
|
|
||||||
|
|
@ -81,101 +81,7 @@ impl Sequencer {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
audio!(|self:Sequencer, 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!(<TuiOut>|self:Sequencer|&self.size);
|
has_size!(<TuiOut>|self:Sequencer|&self.size);
|
||||||
has_clock!(|self:Sequencer|&self.player.clock);
|
has_clock!(|self:Sequencer|&self.player.clock);
|
||||||
has_clips!(|self:Sequencer|self.pool.clips);
|
has_clips!(|self:Sequencer|self.pool.clips);
|
||||||
has_editor!(|self:Sequencer|self.editor);
|
has_editor!(|self:Sequencer|self.editor);
|
||||||
handle!(TuiIn: |self:Sequencer,input|SequencerCommand::execute_with_state(self, input.event()));
|
|
||||||
#[derive(Clone, Debug)] pub enum SequencerCommand {
|
|
||||||
Compact(bool),
|
|
||||||
History(isize),
|
|
||||||
Clock(ClockCommand),
|
|
||||||
Pool(PoolCommand),
|
|
||||||
Editor(MidiEditCommand),
|
|
||||||
Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
|
||||||
}
|
|
||||||
keymap!(KEYS_SEQUENCER = |state: Sequencer, input: Event| SequencerCommand {
|
|
||||||
// TODO: k: toggle on-screen keyboard
|
|
||||||
ctrl(key(Char('k'))) => { todo!("keyboard") },
|
|
||||||
// Transport: Play/pause
|
|
||||||
key(Char(' ')) => Cmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
|
|
||||||
// Transport: Play from start or rewind to start
|
|
||||||
shift(key(Char(' '))) => Cmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
|
|
||||||
// u: undo
|
|
||||||
key(Char('u')) => Cmd::History(-1),
|
|
||||||
// Shift-U: redo
|
|
||||||
key(Char('U')) => Cmd::History( 1),
|
|
||||||
// Tab: Toggle compact mode
|
|
||||||
key(Tab) => Cmd::Compact(!state.compact),
|
|
||||||
// q: Enqueue currently edited clip
|
|
||||||
key(Char('q')) => Cmd::Enqueue(state.pool.clip().clone()),
|
|
||||||
// 0: Enqueue clip 0 (stop all)
|
|
||||||
key(Char('0')) => Cmd::Enqueue(Some(state.clips()[0].clone())),
|
|
||||||
// e: Toggle between editing currently playing or other clip
|
|
||||||
//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
|
|
||||||
});
|
|
||||||
command!(|self: SequencerCommand, state: Sequencer|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(state.pool.clip().as_ref());
|
|
||||||
undo
|
|
||||||
},
|
|
||||||
// update color in all places simultaneously
|
|
||||||
PoolCommand::Phrase(SetColor(index, _)) => {
|
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
|
||||||
state.editor.set_clip(state.pool.clip().as_ref());
|
|
||||||
undo
|
|
||||||
},
|
|
||||||
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
|
||||||
},
|
|
||||||
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
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
||||||
338
tek/src/view.rs
Normal file
338
tek/src/view.rs
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
use crate::*;
|
||||||
|
render!(TuiOut: (self: App) => self.size.of(EdnView::from_source(self, self.edn.as_ref())));
|
||||||
|
audio!(|self: App, _client, _scope|Control::Continue);
|
||||||
|
impl EdnViewData<TuiOut> for &App {
|
||||||
|
fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
|
||||||
|
use EdnItem::*;
|
||||||
|
let w = self.tracks_with_sizes().last().map(|x|x.3 as u16).unwrap_or(0);
|
||||||
|
match item {
|
||||||
|
Nil => Box::new(()),
|
||||||
|
Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
||||||
|
Sym(":editor") => (&self.editor).boxed(),
|
||||||
|
Sym(":inputs") => self.input_row(w, 3).boxed(),
|
||||||
|
Sym(":outputs") => self.output_row(w, 3).boxed(),
|
||||||
|
Sym(":pool") => self.pool().boxed(),
|
||||||
|
Sym(":sample") => self.sample().boxed(),
|
||||||
|
Sym(":sampler") => self.sampler().boxed(),
|
||||||
|
Sym(":scenes") => self.scene_row(w, self.size.h().saturating_sub(9) as u16).boxed(),
|
||||||
|
Sym(":status") => self.status(0).boxed(),
|
||||||
|
Sym(":toolbar") => self.toolbar().boxed(),
|
||||||
|
Sym(":tracks") => self.track_row(w, 3).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 App {
|
||||||
|
fn compact (&self) -> bool { false }
|
||||||
|
fn toolbar (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock))))
|
||||||
|
}
|
||||||
|
fn status (&self, note_pt: usize) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
self.editor.as_ref()
|
||||||
|
.map(|e|Bsp::e(e.clip_status(), e.edit_status()))
|
||||||
|
}
|
||||||
|
fn pool (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
self.pool.as_ref()
|
||||||
|
.map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool))))
|
||||||
|
}
|
||||||
|
fn editor (&self) -> impl Content<TuiOut> + '_ {
|
||||||
|
&self.editor
|
||||||
|
}
|
||||||
|
fn sample <'a> (&'a self) -> impl Content<TuiOut> + 'a {
|
||||||
|
let compact = self.is_editing();
|
||||||
|
if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) {
|
||||||
|
let note_pt = editor.note_point();
|
||||||
|
let sample_h = if compact { 0 } else { 5 };
|
||||||
|
return Some(Max::y(sample_h, Fill::xy(Bsp::a(
|
||||||
|
Fill::x(Align::w(Fixed::y(1, self.status(note_pt)))),
|
||||||
|
sampler.viewer(note_pt)
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn sampler (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
let compact = self.is_editing();
|
||||||
|
if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) {
|
||||||
|
let note_pt = editor.note_point();
|
||||||
|
let w = if compact { 4 } else { 40 };
|
||||||
|
let y = if compact { 1 } else { 0 };
|
||||||
|
return Some(Fixed::x(w, Push::y(y, Fill::y(sampler.list(compact, editor)))))
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row <'a> (
|
||||||
|
&'a self, w: u16, h: u16, a: impl Content<TuiOut> + 'a, b: impl Content<TuiOut> + 'a
|
||||||
|
) -> impl Content<TuiOut> + 'a {
|
||||||
|
Fixed::y(h, Bsp::e(
|
||||||
|
Fixed::xy(self.sidebar_w() as u16, h, a),
|
||||||
|
Fill::x(Align::c(Fixed::xy(w, h, b)))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn track_row (&self, w: u16, h: u16) -> impl Content<TuiOut> + '_ {
|
||||||
|
self.row(w, h, track_header(&self), track_cells(&self))
|
||||||
|
}
|
||||||
|
fn input_row (&self, w: u16, h: u16) -> impl Content<TuiOut> + '_ {
|
||||||
|
self.row(w, h, input_header(&self), input_cells(&self))
|
||||||
|
}
|
||||||
|
fn output_row (&self, w: u16, h: u16) -> impl Content<TuiOut> + '_ {
|
||||||
|
self.row(w, h, output_header(&self), output_cells(&self))
|
||||||
|
}
|
||||||
|
fn scene_row (&self, w: u16, h: u16) -> impl Content<TuiOut> + '_ {
|
||||||
|
self.row(w, h, output_header(&self), output_cells(&self))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tracks_with_sizes (&self)
|
||||||
|
-> impl Iterator<Item = (usize, &ArrangerTrack, usize, usize)>
|
||||||
|
{
|
||||||
|
tracks_with_sizes(self.tracks.iter(), match self.selected {
|
||||||
|
ArrangerSelection::Track(t) if self.is_editing() => Some(t),
|
||||||
|
ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t),
|
||||||
|
_ => None
|
||||||
|
}, self.editor_w())
|
||||||
|
}
|
||||||
|
pub fn scenes_with_sizes (&self, h: usize)
|
||||||
|
-> impl Iterator<Item = (usize, &ArrangerScene, usize, usize)>
|
||||||
|
{
|
||||||
|
scenes_with_sizes(self.scenes.iter(), &self.selected, self.is_editing(), 2, 15)
|
||||||
|
}
|
||||||
|
fn is_editing (&self) -> bool {
|
||||||
|
self.editing.load(Relaxed)
|
||||||
|
}
|
||||||
|
fn editor_w (&self) -> usize {
|
||||||
|
let editor = self.editor.as_ref().expect("missing editor");
|
||||||
|
(5 + (editor.time_len().get() / editor.time_zoom().get()))
|
||||||
|
.min(self.size.w().saturating_sub(20))
|
||||||
|
.max(16)
|
||||||
|
}
|
||||||
|
fn sidebar_w (&self) -> u16 {
|
||||||
|
let w = self.size.w();
|
||||||
|
let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||||
|
let w = if self.is_editing() { 8 } else { w };
|
||||||
|
w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn scenes_with_sizes <'a>(
|
||||||
|
scenes: impl Iterator<Item=&'a ArrangerScene> + 'a,
|
||||||
|
selected: &'a ArrangerSelection,
|
||||||
|
editing: bool,
|
||||||
|
scene_height: usize,
|
||||||
|
scene_larger: usize,
|
||||||
|
) -> impl Iterator<Item = (usize, &'a ArrangerScene, usize, usize)> + 'a {
|
||||||
|
let mut y = 0;
|
||||||
|
let (selected_track, selected_scene) = match selected {
|
||||||
|
ArrangerSelection::Clip(t, s) => (Some(t), Some(s)),
|
||||||
|
_ => (None, None)
|
||||||
|
};
|
||||||
|
scenes.enumerate().map(move|(s, scene)|{
|
||||||
|
let active = editing && selected_track.is_some() && selected_scene == Some(&s);
|
||||||
|
let height = if active { scene_larger } else { scene_height };
|
||||||
|
let data = (s, scene, y, y + height);
|
||||||
|
y += height;
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn tracks_with_sizes <'a> (
|
||||||
|
tracks: impl Iterator<Item=&'a ArrangerTrack>,
|
||||||
|
active: Option<usize>,
|
||||||
|
bigger: usize
|
||||||
|
) -> impl Iterator<Item=(usize,&'a ArrangerTrack,usize,usize)> {
|
||||||
|
let mut x = 0;
|
||||||
|
tracks.enumerate().map(move |(index, track)|{
|
||||||
|
let width = if Some(index) == active { bigger } else { track.width.max(8) };
|
||||||
|
let data = (index, track, x, x + width);
|
||||||
|
x += width;
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn track_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
||||||
|
(||Tui::bg(TuiTheme::g(32), Tui::bold(true, Bsp::s(
|
||||||
|
row!(
|
||||||
|
Tui::fg(TuiTheme::g(128), "add "),
|
||||||
|
Tui::fg(TuiTheme::orange(), "t"),
|
||||||
|
Tui::fg(TuiTheme::g(128), "rack"),
|
||||||
|
),
|
||||||
|
row!(
|
||||||
|
Tui::fg(TuiTheme::orange(), "a"),
|
||||||
|
Tui::fg(TuiTheme::g(128), "dd scene"),
|
||||||
|
),
|
||||||
|
))).boxed()).into()
|
||||||
|
}
|
||||||
|
pub fn track_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
||||||
|
let iter = ||state.tracks_with_sizes();
|
||||||
|
(move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| {
|
||||||
|
let name = Push::x(1, &track.name);
|
||||||
|
let color = track.color;
|
||||||
|
let fg = color.lightest.rgb;
|
||||||
|
let bg = color.base.rgb;
|
||||||
|
let active = state.selected.track() == Some(i);
|
||||||
|
let bfg = if active { Color::Rgb(255,255,255) } else { Color::Rgb(0,0,0) };
|
||||||
|
let border = Style::default().fg(bfg).bg(bg);
|
||||||
|
Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16,
|
||||||
|
Outer(border).enclose(Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::x(name)))))
|
||||||
|
))
|
||||||
|
})).boxed()).into()
|
||||||
|
}
|
||||||
|
fn help_tag <'a>(before: &'a str, key: &'a str, after: &'a str) -> impl Content<TuiOut> + 'a {
|
||||||
|
let lo = TuiTheme::g(128);
|
||||||
|
let hi = TuiTheme::orange();
|
||||||
|
Tui::bold(true, row!(Tui::fg(lo, before), Tui::fg(hi, key), Tui::fg(lo, after)))
|
||||||
|
}
|
||||||
|
fn input_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
||||||
|
let fg = TuiTheme::g(224);
|
||||||
|
let bg = TuiTheme::g(64);
|
||||||
|
(move||Bsp::s(help_tag("midi ", "I", "ns"), state.midi_ins.get(0).map(|inp|Bsp::s(
|
||||||
|
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(inp.name.clone())))),
|
||||||
|
inp.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false,
|
||||||
|
Tui::fg_bg(fg, bg, connect.info()))))),
|
||||||
|
))).boxed()).into()
|
||||||
|
}
|
||||||
|
fn input_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
||||||
|
(move||Align::x(Map::new(||state.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
||||||
|
let w = (x2 - x1) as u16;
|
||||||
|
let color: ItemPalette = track.color.dark.into();
|
||||||
|
map_east(x1 as u16, w, Fixed::x(w, cell(color, Bsp::n(
|
||||||
|
rec_mon(color.base.rgb, false, false),
|
||||||
|
phat_hi(color.base.rgb, color.dark.rgb)
|
||||||
|
))))
|
||||||
|
})).boxed()).into()
|
||||||
|
}
|
||||||
|
fn output_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
||||||
|
let fg = TuiTheme::g(224);
|
||||||
|
let bg = TuiTheme::g(64);
|
||||||
|
(move||Bsp::s(help_tag("midi ", "O", "uts"), state.midi_outs.get(0).map(|out|Bsp::s(
|
||||||
|
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(out.name.clone())))),
|
||||||
|
out.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false,
|
||||||
|
Tui::fg_bg(fg, bg, connect.info()))))),
|
||||||
|
))).boxed()).into()
|
||||||
|
}
|
||||||
|
fn output_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
||||||
|
(move||Align::x(Map::new(||state.tracks_with_sizes(), move|(_, track, x1, x2), i| {
|
||||||
|
let w = (x2 - x1) as u16;
|
||||||
|
let color: ItemPalette = track.color.dark.into();
|
||||||
|
map_east(x1 as u16, w, Fixed::x(w, cell(color, Bsp::n(
|
||||||
|
mute_solo(color.base.rgb, false, false),
|
||||||
|
phat_hi(color.dark.rgb, color.darker.rgb)
|
||||||
|
))))
|
||||||
|
})).boxed()).into()
|
||||||
|
}
|
||||||
|
fn scene_headers <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
||||||
|
(||{
|
||||||
|
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
|
||||||
|
let selected = state.selected.scene();
|
||||||
|
Fill::y(Align::c(Map::new(||state.scenes_with_sizes(2), move|(_, scene, y1, y2), i| {
|
||||||
|
let h = (y2 - y1) as u16;
|
||||||
|
let name = format!("🭬{}", &scene.name);
|
||||||
|
let color = scene.color;
|
||||||
|
let active = selected == Some(i);
|
||||||
|
let mid = if active { color.light } else { color.base };
|
||||||
|
let top = Some(last_color.read().unwrap().base.rgb);
|
||||||
|
let cell = phat_sel_3(
|
||||||
|
active,
|
||||||
|
Tui::bold(true, name.clone()),
|
||||||
|
Tui::bold(true, name),
|
||||||
|
top,
|
||||||
|
mid.rgb,
|
||||||
|
Color::Rgb(0, 0, 0)
|
||||||
|
);
|
||||||
|
*last_color.write().unwrap() = color;
|
||||||
|
map_south(y1 as u16, h + 1, Fixed::y(h + 1, cell))
|
||||||
|
}))).boxed()
|
||||||
|
}).into()
|
||||||
|
}
|
||||||
|
fn scene_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
||||||
|
let editing = state.is_editing();
|
||||||
|
let tracks = move||state.tracks_with_sizes();
|
||||||
|
let scenes = ||state.scenes_with_sizes(2);
|
||||||
|
let selected_track = state.selected.track();
|
||||||
|
let selected_scene = state.selected.scene();
|
||||||
|
(move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| {
|
||||||
|
let w = (x2 - x1) as u16;
|
||||||
|
let color: ItemPalette = track.color.dark.into();
|
||||||
|
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
|
||||||
|
let cells = Map::new(scenes, move|(_, scene, y1, y2), s| {
|
||||||
|
let h = (y2 - y1) as u16;
|
||||||
|
let color = scene.color;
|
||||||
|
let (name, fg, bg) = if let Some(c) = &scene.clips[t] {
|
||||||
|
let c = c.read().unwrap();
|
||||||
|
(c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb)
|
||||||
|
} else {
|
||||||
|
("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32))
|
||||||
|
};
|
||||||
|
let last = last_color.read().unwrap().clone();
|
||||||
|
let active = editing && selected_scene == Some(s) && selected_track == Some(t);
|
||||||
|
let editor = Thunk::new(||&state.editor);
|
||||||
|
let cell = Thunk::new(move||phat_sel_3(
|
||||||
|
selected_track == Some(t) && selected_scene == Some(s),
|
||||||
|
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
|
||||||
|
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
|
||||||
|
if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(bg.into())
|
||||||
|
},
|
||||||
|
bg.into(),
|
||||||
|
bg.into(),
|
||||||
|
));
|
||||||
|
let cell = Either(active, editor, cell);
|
||||||
|
*last_color.write().unwrap() = bg.into();
|
||||||
|
map_south(
|
||||||
|
y1 as u16,
|
||||||
|
h + 1,
|
||||||
|
Fill::x(Fixed::y(h + 1, cell))
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed());
|
||||||
|
Fixed::x(w, map_east(x1 as u16, w, column))
|
||||||
|
}))).boxed()).into()
|
||||||
|
}
|
||||||
|
fn cell_clip <'a> (
|
||||||
|
scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16
|
||||||
|
) -> impl Content<TuiOut> + use<'a> {
|
||||||
|
scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{
|
||||||
|
let clip = clip.read().unwrap();
|
||||||
|
let mut bg = TuiTheme::border_bg();
|
||||||
|
let name = clip.name.to_string();
|
||||||
|
let max_w = name.len().min((w as usize).saturating_sub(2));
|
||||||
|
let color = clip.color;
|
||||||
|
bg = color.dark.rgb;
|
||||||
|
if let Some((_, Some(ref playing))) = track.player.play_clip() {
|
||||||
|
if *playing.read().unwrap() == *clip {
|
||||||
|
bg = color.light.rgb
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Fixed::xy(w, h, &Tui::bg(bg, Push::x(1, Fixed::x(w, &name.as_str()[0..max_w]))));
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
fn rec_mon (bg: Color, rec: bool, mon: bool) -> impl Content<TuiOut> {
|
||||||
|
row!(
|
||||||
|
Tui::fg_bg(if rec { Color::Red } else { bg }, bg, "▐"),
|
||||||
|
Tui::fg_bg(if rec { Color::White } else { Color::Rgb(0,0,0) }, bg, "REC"),
|
||||||
|
Tui::fg_bg(if rec { Color::White } else { bg }, bg, "▐"),
|
||||||
|
Tui::fg_bg(if mon { Color::White } else { Color::Rgb(0,0,0) }, bg, "MON"),
|
||||||
|
Tui::fg_bg(if mon { Color::White } else { bg }, bg, "▌"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content<TuiOut> {
|
||||||
|
row!(
|
||||||
|
Tui::fg_bg(if mute { Color::White } else { Color::Rgb(0,0,0) }, bg, "MUTE"),
|
||||||
|
Tui::fg_bg(if mute { Color::White } else { bg }, bg, "▐"),
|
||||||
|
Tui::fg_bg(if solo { Color::White } else { Color::Rgb(0,0,0) }, bg, "SOLO"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn cell <T: Content<TuiOut>> (color: ItemPalette, field: T) -> impl Content<TuiOut> {
|
||||||
|
Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field))
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue