From 7ff1d989a9e2de925157177b699f3c8ab7ea2dc5 Mon Sep 17 00:00:00 2001 From: okay stopped screaming Date: Sat, 21 Mar 2026 20:33:51 +0200 Subject: [PATCH 1/7] well, 55 errors until up to date and then we fix --- app/tek.rs | 159 +++++++++---------- app/tek_impls.rs | 386 ++++++++++++++++++++++------------------------ app/tek_struct.rs | 14 +- app/tek_trait.rs | 116 +++++++------- tengri | 2 +- 5 files changed, 333 insertions(+), 344 deletions(-) diff --git a/app/tek.rs b/app/tek.rs index d7279804..828827f1 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -22,10 +22,13 @@ pub(crate) use tengri::{ *, lang::*, play::*, + keys::*, sing::*, time::*, draw::*, - tui::*, + term::*, + color::*, + space::*, crossterm::event::{Event, KeyEvent}, ratatui::{ self, @@ -953,13 +956,13 @@ mod view { /// ``` /// let _ = tek::view_logo(); /// ``` - pub fn view_logo () -> impl Content { - Fixed::XY(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{ - Fixed::Y(1, ""), - Fixed::Y(1, ""), - Fixed::Y(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), - Fixed::Y(1, Bsp::e("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), - Fixed::Y(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), + pub fn view_logo () -> impl Draw { + wh_exact(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), south!{ + h_exact(1, ""), + h_exact(1, ""), + h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), + h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), + h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), }))) } @@ -973,14 +976,14 @@ mod view { bpm: Arc>, beat: Arc>, time: Arc>, - ) -> impl Content { + ) -> impl Draw { let theme = ItemTheme::G[96]; - Tui::bg(Black, row!(Bsp::a( - Fill::XY(Align::w(button_play_pause(play))), - Fill::XY(Align::e(row!( - FieldH(theme, "BPM", bpm), - FieldH(theme, "Beat", beat), - FieldH(theme, "Time", time), + Tui::bg(Black, east!(above( + wh_full(origin_w(button_play_pause(play))), + wh_full(origin_e(east!( + field_h(theme, "BPM", bpm), + field_h(theme, "Beat", beat), + field_h(theme, "Time", time), ))) ))) } @@ -995,14 +998,14 @@ mod view { sr: Arc>, buf: Arc>, lat: Arc>, - ) -> impl Content { + ) -> impl Draw { let theme = ItemTheme::G[96]; - Tui::bg(Black, row!(Bsp::a( - Fill::XY(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))), - Fill::XY(Align::e(row!( - FieldH(theme, "SR", sr), - FieldH(theme, "Buf", buf), - FieldH(theme, "Lat", lat), + Tui::bg(Black, east!(above( + wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))), + wh_full(origin_e(east!( + field_h(theme, "SR", sr), + field_h(theme, "Buf", buf), + field_h(theme, "Lat", lat), ))) ))) } @@ -1010,17 +1013,17 @@ mod view { /// ``` /// let _ = tek::button_play_pause(true); /// ``` - pub fn button_play_pause (playing: bool) -> impl Content { + pub fn button_play_pause (playing: bool) -> impl Draw { let compact = true;//self.is_editing(); Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - Either::new(compact, - Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(9, Either::new(playing, + either(compact, + Thunk::new(move|to: &mut Tui|to.place(&w_exact(9, either(playing, Tui::fg(Rgb(0, 255, 0), " PLAYING "), Tui::fg(Rgb(255, 128, 0), " STOPPED "))) )), - Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(5, Either::new(playing, - Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))) + Thunk::new(move|to: &mut Tui|to.place(&w_exact(5, either(playing, + Tui::fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + Tui::fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)))) )) ) ) @@ -1028,12 +1031,12 @@ mod view { #[cfg(feature = "track")] pub fn view_track_row_section ( _theme: ItemTheme, - button: impl Content, - button_add: impl Content, - content: impl Content, - ) -> impl Content { - Bsp::w(Fill::Y(Fixed::X(4, Align::nw(button_add))), - Bsp::e(Fixed::X(20, Fill::Y(Align::nw(button))), Fill::XY(Align::c(content)))) + button: impl Draw, + button_add: impl Draw, + content: impl Draw, + ) -> impl Draw { + west(h_full(w_exact(4, origin_nw(button_add))), + east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content)))) } /// ``` @@ -1041,20 +1044,20 @@ mod view { /// let fg = tengri::ratatui::style::Color::Green; /// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); /// ``` - pub fn view_wrap (bg: Color, fg: Color, content: impl Content) -> impl Content { - let left = Tui::fg_bg(bg, Reset, Fixed::X(1, Repeat::Y("▐"))); - let right = Tui::fg_bg(bg, Reset, Fixed::X(1, Repeat::Y("▌"))); - Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content))) + pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw { + let left = Tui::fg_bg(bg, Reset, w_exact(1, y_repeat("▐"))); + let right = Tui::fg_bg(bg, Reset, w_exact(1, y_repeat("▌"))); + east(left, west(right, Tui::fg_bg(fg, bg, content))) } /// ``` /// let _ = tek::view_meter("", 0.0); /// let _ = tek::view_meters(&[0.0, 0.0]); /// ``` - pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Content + 'a { - col!( - FieldH(ItemTheme::G[128], label, format!("{:>+9.3}", value)), - Fixed::XY(if value >= 0.0 { 13 } + pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { + south!( + field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)), + wh_exact(if value >= 0.0 { 13 } else if value >= -1.0 { 12 } else if value >= -2.0 { 11 } else if value >= -3.0 { 10 } @@ -1072,47 +1075,47 @@ mod view { else { Green }, ()))) } - pub fn view_meters (values: &[f32;2]) -> impl Content + use<'_> { + pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { let left = format!("L/{:>+9.3}", values[0]); let right = format!("R/{:>+9.3}", values[1]); - Bsp::s(left, right) + south(left, right) } - pub fn draw_info (sample: Option<&Arc>>) -> impl Content + use<'_> { - When::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ + pub fn draw_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { + when(sample.is_some(), Thunk::new(move|to: &mut Tui|{ let sample = sample.unwrap().read().unwrap(); let theme = sample.color; - to.place(&row!( - FieldH(theme, "Name", format!("{:<10}", sample.name.clone())), - FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())), - FieldH(theme, "Start", format!("{:<8}", sample.start)), - FieldH(theme, "End", format!("{:<8}", sample.end)), - FieldH(theme, "Trans", "0"), - FieldH(theme, "Gain", format!("{}", sample.gain)), + to.place(&east!( + field_h(theme, "Name", format!("{:<10}", sample.name.clone())), + field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), + field_h(theme, "Start", format!("{:<8}", sample.start)), + field_h(theme, "End", format!("{:<8}", sample.end)), + field_h(theme, "Trans", "0"), + field_h(theme, "Gain", format!("{}", sample.gain)), )) })) } - pub fn draw_info_v (sample: Option<&Arc>>) -> impl Content + use<'_> { - Either::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ + pub fn draw_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { + either(sample.is_some(), Thunk::new(move|to: &mut Tui|{ let sample = sample.unwrap().read().unwrap(); let theme = sample.color; - to.place(&Fixed::X(20, col!( - Fill::X(Align::w(FieldH(theme, "Name ", format!("{:<10}", sample.name.clone())))), - Fill::X(Align::w(FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())))), - Fill::X(Align::w(FieldH(theme, "Start ", format!("{:<8}", sample.start)))), - Fill::X(Align::w(FieldH(theme, "End ", format!("{:<8}", sample.end)))), - Fill::X(Align::w(FieldH(theme, "Trans ", "0"))), - Fill::X(Align::w(FieldH(theme, "Gain ", format!("{}", sample.gain)))), + to.place(&w_exact(20, south!( + w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))), + w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))), + w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))), + w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))), + w_full(origin_w(field_h(theme, "Trans ", "0"))), + w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))), ))) - }), Thunk::new(|to: &mut TuiOut|to.place(&Tui::fg(Red, col!( + }), Thunk::new(|to: &mut Tui|to.place(&Tui::fg(Red, south!( Tui::bold(true, "× No sample."), "[r] record", "[Shift-F9] import", ))))) } - pub fn draw_status (sample: Option<&Arc>>) -> impl Content { + pub fn draw_status (sample: Option<&Arc>>) -> impl Draw { Tui::bold(true, Tui::fg(Tui::g(224), sample .map(|sample|{ let sample = sample.read().unwrap(); @@ -1121,31 +1124,31 @@ mod view { .unwrap_or_else(||"No sample".to_string()))) } - pub fn view_track_header (theme: ItemTheme, content: impl Content) -> impl Content { - Fixed::X(12, Tui::bg(theme.darker.rgb, Fill::X(Align::e(content)))) + pub fn view_track_header (theme: ItemTheme, content: impl Draw) -> impl Draw { + w_exact(12, Tui::bg(theme.darker.rgb, w_full(origin_e(content)))) } pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) - -> impl Content + use<'a, T> + -> impl Draw + use<'a, T> { let ins = ports.len() as u16; let frame = Outer(true, Style::default().fg(Tui::g(96))); let iter = move||ports.iter(); - let names = Map::south(1, iter, move|port, index|Fill::Y(Align::w(format!(" {index} {}", port.port_name())))); - let field = FieldV(theme, title, names); - Fixed::XY(20, 1 + ins, frame.enclose(Fixed::XY(20, 1 + ins, field))) + let names = iter_south(1, iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); + let field = field_v(theme, title, names); + wh_exact(20, 1 + ins, frame.enclose(wh_exact(20, 1 + ins, field))) } pub fn io_ports <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a - ) -> impl Content + 'a { - Map::new(iter, move|( + fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a + ) -> impl Draw + 'a { + iter(items, move|( _index, name, connections, y, y2 ): (usize, &'a Arc, &'a [Connect], usize, usize), _| - map_south(y as u16, (y2-y) as u16, Bsp::s( - Fill::Y(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(&" 󰣲 ", name))))), - Map::new(||connections.iter(), move|connect: &'a Connect, index|map_south(index as u16, 1, - Fill::Y(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, + iter_south(y as u16, (y2-y) as u16, south( + h_full(Tui::bold(true, Tui::fg_bg(fg, bg, origin_w(east(&" 󰣲 ", name))))), + iter(||connections.iter(), move|connect: &'a Connect, index|iter_south(index as u16, 1, + h_full(origin_w(Tui::bold(false, Tui::fg_bg(fg, bg, &connect.info))))))))) } } @@ -1210,5 +1213,5 @@ pub const NOTE_NAMES: [&str; 128] = [ pub(crate) const HEADER: &'static str = r#" ~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ - █ █▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~ + █ term█▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~ ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; diff --git a/app/tek_impls.rs b/app/tek_impls.rs index 67a6a9d1..b6b498cf 100644 --- a/app/tek_impls.rs +++ b/app/tek_impls.rs @@ -64,18 +64,18 @@ use std::fmt::Write; } } -impl +AsMut> HasClock for T {} -impl +AsMut> HasSelection for T {} -impl +AsMut> HasSequencer for T {} -impl >+AsMut>> HasScenes for T {} -impl >+AsMut>> HasTracks for T {} -impl +AsMutOpt> HasEditor for T {} -impl +AsMutOpt+Send+Sync> HasScene for T {} -impl +AsMutOpt+Send+Sync> HasTrack for T {} -impl MidiPoint for T {} -impl > TracksView for T {} -impl MidiRange for T {} -impl ClipsView for T {} +impl +AsMut> HasClock for T {} +impl +AsMut> HasSelection for T {} +impl +AsMut> HasSequencer for T {} +impl >+AsMut>> HasScenes for T {} +impl >+AsMut>> HasTracks for T {} +impl +AsMutOpt> HasEditor for T {} +impl +AsMutOpt+Send+Sync> HasScene for T {} +impl +AsMutOpt+Send+Sync> HasTrack for T {} +impl MidiPoint for T {} +impl > TracksView for T {} +impl MidiRange for T {} +impl ClipsView for T {} mod app { use crate::*; @@ -84,24 +84,24 @@ mod app { impl_has!(Vec: |self: App|self.project.midi_outs); impl_has!(Dialog: |self: App|self.dialog); impl_has!(Jack<'static>: |self: App|self.jack); - impl_has!(Measure: |self: App|self.size); + impl_has!(Measure: |self: App|self.size); impl_has!(Pool: |self: App|self.pool); impl_has!(Selection: |self: App|self.project.selection); impl_as_ref!(Vec: |self: App|self.project.as_ref()); impl_as_mut!(Vec: |self: App|self.project.as_mut()); impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); - has_clips!( |self: App|self.pool.clips); + impl_has_clips!( |self: App|self.pool.clips); impl_audio!(App: tek_jack_process, tek_jack_event); - handle!(TuiIn: |self: App, input|{ + impl_handle!(TuiIn: |self: App, input|{ let commands = collect_commands(self, input)?; let history = execute_commands(self, commands)?; self.history.extend(history.into_iter()); Ok(None) }); - impl Draw for App { - fn draw (&self, to: &mut TuiOut) { + impl Draw for App { + fn draw (self, to: &mut Tui) -> Usually> { if let Some(e) = self.error.read().unwrap().as_ref() { to.show(to.area(), e); } @@ -125,16 +125,16 @@ mod app { }); } - impl Understand for App { - fn understand_expr <'a> (&'a self, to: &mut TuiOut, lang: &'a impl Expression) -> Usually<()> { + impl Understand for App { + fn understand_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { app_understand_expr(self, to, lang) } - fn understand_word <'a> (&'a self, to: &mut TuiOut, lang: &'a impl Expression) -> Usually<()> { + fn understand_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { app_understand_word(self, to, lang) } } - fn app_understand_expr (state: &App, to: &mut TuiOut, lang: &impl Expression) -> Usually<()> { + fn app_understand_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> { if evaluate_output_expression(state, to, lang)? || evaluate_output_expression_tui(state, to, lang)? { Ok(()) @@ -143,22 +143,22 @@ mod app { } } - fn app_understand_word (state: &App, to: &mut TuiOut, dsl: &impl Expression) -> Usually<()> { + fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> { let mut frags = dsl.src()?.unwrap().split("/"); match frags.next() { Some(":logo") => to.place(&view_logo()), - Some(":status") => to.place(&Fixed::Y(1, "TODO: Status Bar")), + Some(":status") => to.place(&h_exact(1, "TODO: Status Bar")), Some(":meters") => match frags.next() { - Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Input Meters")))), - Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Output Meters")))), + Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters")))), + Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters")))), _ => panic!() }, Some(":tracks") => match frags.next() { None => to.place(&"TODO tracks"), - Some("names") => to.place(&state.project.view_track_names(state.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Names")))), - Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Inputs")))), - Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Devices")))), - Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Outputs")))), + Some("names") => to.place(&state.project.view_track_names(state.color.clone())),//Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), + Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs")))), + Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices")))), + Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs")))), _ => panic!() }, Some(":scenes") => match frags.next() { @@ -171,13 +171,13 @@ mod app { Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &state.dialog { let items = items.clone(); let selected = selected; - Some(Fill::XY(Thunk::new(move|to: &mut TuiOut|{ + Some(wh_full(Thunk::new(move|to: &mut Tui|{ for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - to.place(&Push::Y((2 * index) as u16, + to.place(&y_push((2 * index) as u16, Tui::fg_bg( if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, - Fixed::Y(2, Align::n(Fill::X(item))) + h_exact(2, origin_n(w_full(item))) ))); } }))) @@ -189,31 +189,31 @@ mod app { Some(":templates") => to.place(&{ let modes = state.config.modes.clone(); let height = (modes.read().unwrap().len() * 2) as u16; - Fixed::Y(height, Min::X(30, Thunk::new(move |to: &mut TuiOut|{ + h_exact(height, w_min(30, Thunk::new(move |to: &mut Tui|{ for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); let fg1 = Rgb(224, 192, 128); let fg2 = Rgb(224, 128, 32); - let field_name = Fill::X(Align::w(Tui::fg(fg1, name))); - let field_id = Fill::X(Align::e(Tui::fg(fg2, id))); - let field_info = Fill::X(Align::w(info)); - to.place(&Push::Y((2 * index) as u16, - Fixed::Y(2, Fill::X(Tui::bg(bg, Bsp::s( - Bsp::a(field_name, field_id), field_info)))))); + let field_name = w_full(origin_w(Tui::fg(fg1, name))); + let field_id = w_full(origin_e(Tui::fg(fg2, id))); + let field_info = w_full(origin_w(info)); + to.place(&y_push((2 * index) as u16, + h_exact(2, w_full(Tui::bg(bg, south( + above(field_name, field_id), field_info)))))); } }))) }), - Some(":sessions") => to.place(&Fixed::Y(6, Min::X(30, Thunk::new(|to: &mut TuiOut|{ + Some(":sessions") => to.place(&h_exact(6, w_min(30, Thunk::new(|to: &mut Tui|{ let fg = Rgb(224, 192, 128); for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; - to.place(&Push::Y((2 * index) as u16, - &Fixed::Y(2, Fill::X(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); + to.place(&y_push((2 * index) as u16, + &h_exact(2, w_full(Tui::bg(bg, origin_w(Tui::fg(fg, name))))))); } })))), - Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(ItemColor::default(), + Some(":browse/title") => to.place(&w_full(origin_w(field_v(ItemColor::default(), match state.dialog.browser_target().unwrap() { BrowseTarget::SaveProject => "Save project:", BrowseTarget::LoadProject => "Load project:", @@ -221,18 +221,18 @@ mod app { BrowseTarget::ExportSample(_) => "Export sample:", BrowseTarget::ImportClip(_) => "Import clip:", BrowseTarget::ExportClip(_) => "Export clip:", - }, Shrink::X(3, Fixed::Y(1, Tui::fg(Tui::g(96), Repeat::X("🭻")))))))), + }, w_shrink(3, h_exact(1, Tui::fg(Tui::g(96), x_repeat("🭻")))))))), Some(":device") => { let selected = state.dialog.device_kind().unwrap(); - to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1, + to.place(&south(Tui::bold(true, "Add device"), iter_south(1, move||device_kinds().iter(), move|_label: &&'static str, i|{ let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; let lb = if i == selected { "[ " } else { " " }; let rb = if i == selected { " ]" } else { " " }; - Fill::X(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) + w_full(Tui::bg(bg, east(lb, west(rb, "FIXME device name")))) }))) }, - Some(":debug") => to.place(&Fixed::Y(1, format!("[{:?}]", to.area()))), + Some(":debug") => to.place(&h_exact(1, format!("[{:?}]", to.area()))), Some(_) => { let views = state.config.views.read().unwrap(); if let Some(dsl) = views.get(dsl.src()?.unwrap()) { @@ -375,7 +375,7 @@ mod app { mod arrange { use crate::*; impl_has!(Jack<'static>: |self: Arrangement| self.jack); - impl_has!(Measure: |self: Arrangement| self.size); + impl_has!(Measure: |self: Arrangement| self.size); impl_has!(Vec: |self: Arrangement| self.tracks); impl_has!(Vec: |self: Arrangement| self.scenes); impl_has!(Vec: |self: Arrangement| self.midi_ins); @@ -479,79 +479,79 @@ mod arrange { } Ok((index, &mut self.tracks_mut()[index])) } - #[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Content + '_ { - Bsp::s( - Fixed::Y(1, self.view_inputs_header()), - Thunk::new(|to: &mut TuiOut|{ + #[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { + south( + h_exact(1, self.view_inputs_header()), + Thunk::new(|to: &mut Tui|{ for (index, port) in self.midi_ins().iter().enumerate() { - to.place(&Push::X(index as u16 * 10, Fixed::Y(1, self.view_inputs_row(port)))) + to.place(&x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port)))) } }) ) } - #[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Content + '_ { - Bsp::e(Fixed::X(20, Align::w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), - Bsp::w(Fixed::X(4, button_2("I", "+", false)), Thunk::new(move|to: &mut TuiOut|for (_index, track, x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Draw + '_ { + east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), + west(w_exact(4, button_2("I", "+", false)), Thunk::new(move|to: &mut Tui|for (_index, track, x1, _x2) in self.tracks_with_sizes() { #[cfg(feature = "track")] - to.place(&Push::X(x1 as u16, Tui::bg(track.color.dark.rgb, Align::w(Fixed::X(track.width as u16, row!( - Either::new(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), - Either::new(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), - Either::new(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), + to.place(&x_push(x1 as u16, Tui::bg(track.color.dark.rgb, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), + either(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), + either(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), )))))) }))) } - #[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Content { - Bsp::e(Fixed::X(20, Align::w(Bsp::e(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), - Bsp::w(Fixed::X(4, ()), Thunk::new(move|to: &mut TuiOut|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Draw { + east(w_exact(20, origin_w(east(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), + west(w_exact(4, ()), Thunk::new(move|to: &mut Tui|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { #[cfg(feature = "track")] - to.place(&Tui::bg(track.color.darker.rgb, Align::w(Fixed::X(track.width as u16, row!( - Either::new(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), - Either::new(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), - Either::new(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), + to.place(&Tui::bg(track.color.darker.rgb, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), + either(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), + either(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), ))))) }))) } - #[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Content { + #[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { let mut h = 1; for output in self.midi_outs().iter() { h += 1 + output.connections.len(); } let h = h as u16; - let list = Bsp::s( - Fixed::Y(1, Fill::X(Align::w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), - Fixed::Y(h - 1, Fill::XY(Align::nw(Thunk::new(|to: &mut TuiOut|{ + let list = south( + h_exact(1, w_full(origin_w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), + h_exact(h - 1, wh_full(origin_nw(Thunk::new(|to: &mut Tui|{ for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&Fixed::Y(1,Fill::X(Bsp::e( - Align::w(Bsp::e(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), - Fill::X(Align::e(format!("{}/{} ", + to.place(&h_exact(1,w_full(east( + origin_w(east(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), + w_full(origin_e(format!("{}/{} ", port.port().get_connections().len(), port.connections.len()))))))); for (index, conn) in port.connections.iter().enumerate() { - to.place(&Fixed::Y(1, Fill::X(Align::w(format!(" c{index:02}{}", conn.info()))))); + to.place(&h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info()))))); } } }))))); - Fixed::Y(h, view_track_row_section(theme, list, button_2("O", "+", false), - Tui::bg(theme.darker.rgb, Align::w(Fill::X( - Thunk::new(|to: &mut TuiOut|{ + h_exact(h, view_track_row_section(theme, list, button_2("O", "+", false), + Tui::bg(theme.darker.rgb, origin_w(w_full( + Thunk::new(|to: &mut Tui|{ for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Fixed::X(track_width(index, track), - Thunk::new(|to: &mut TuiOut|{ - to.place(&Fixed::Y(1, Align::w(Bsp::e( - Either::new(true, Tui::fg(Green, "play "), "play "), - Either::new(false, Tui::fg(Yellow, "solo "), "solo "), + to.place(&w_exact(track_width(index, track), + Thunk::new(|to: &mut Tui|{ + to.place(&h_exact(1, origin_w(east( + either(true, Tui::fg(Green, "play "), "play "), + either(false, Tui::fg(Yellow, "solo "), "solo "), )))); for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&Fixed::Y(1, Align::w(Bsp::e( - Either::new(true, Tui::fg(Green, " ● "), " · "), - Either::new(false, Tui::fg(Yellow, " ● "), " · "), + to.place(&h_exact(1, origin_w(east( + either(true, Tui::fg(Green, " ● "), " · "), + either(false, Tui::fg(Yellow, " ● "), " · "), )))); for (_index, _conn) in port.connections.iter().enumerate() { - to.place(&Fixed::Y(1, Fill::X(""))); + to.place(&h_exact(1, w_full(""))); } }})))}})))))) } - #[cfg(feature = "track")] pub fn view_track_devices (&self, theme: ItemTheme) -> impl Content { + #[cfg(feature = "track")] pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { let mut h = 2u16; for track in self.tracks().iter() { h = h.max(track.devices.len() as u16 * 2); @@ -559,14 +559,14 @@ mod arrange { view_track_row_section(theme, button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), button_2("D", "+", false), - Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Fixed::XY(track_width(index, track), h + 1, - Tui::bg(track.color.dark.rgb, Align::nw(Map::south(2, move||0..h, - |_, _index|Fixed::XY(track.width as u16, 2, + Thunk::new(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&wh_exact(track_width(index, track), h + 1, + Tui::bg(track.color.dark.rgb, origin_nw(iter_south(2, move||0..h, + |_, _index|wh_exact(track.width as u16, 2, Tui::fg_bg( ItemTheme::G[32].lightest.rgb, ItemTheme::G[32].dark.rgb, - Align::nw(format!(" · {}", "--"))))))))); + origin_nw(format!(" · {}", "--"))))))))); })) } /// Put a clip in a slot @@ -616,7 +616,7 @@ mod arrange { } } impl HasClipsSize for Arrangement { - fn clips_size (&self) -> &Measure { &self.size_inner } + fn clips_size (&self) -> &Measure { &self.size_inner } } } @@ -657,14 +657,14 @@ mod browse { fn _todo_stub_usize (&self) -> usize { todo!() } fn _todo_stub_arc_str (&self) -> Arc { todo!() } } - impl HasContent for Browse { - fn content (&self) -> impl Content { - Map::south(1, ||EntriesIterator { + impl Browse { + fn tui (&self) -> impl Draw { + iter_south(1, ||EntriesIterator { offset: 0, index: 0, length: self.dirs.len() + self.files.len(), browser: self, - }, |entry, _index|Fill::X(Align::w(entry))) + }, |entry, _index|w_full(origin_w(entry))) } } impl<'a> Iterator for EntriesIterator<'a> { @@ -1117,7 +1117,7 @@ impl InteriorMutable for AtomicUsize { fn set (&self, value: usize) -> us impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } impl AsRef> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } } -impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.size_inner } } +impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.size_inner } } impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } @@ -1795,11 +1795,8 @@ mod audio { mod meter { use crate::*; - impl Layout for RmsMeter {} - impl Layout for Log10Meter {} - - impl Draw for RmsMeter { - fn draw (&self, to: &mut TuiOut) { + impl Draw for RmsMeter { + fn draw(self, to: &mut Tui) -> Usually> { let XYWH(x, y, w, h) = to.area(); let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); let v = (signal * h as f32).ceil() as u16; @@ -1813,8 +1810,8 @@ mod audio { } } - impl Draw for Log10Meter { - fn draw (&self, to: &mut TuiOut) { + impl Draw for Log10Meter { + fn draw(self, to: &mut Tui) -> Usually> { let XYWH(x, y, w, h) = to.area(); let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); let v = (signal * h as f32 / 100.0).ceil() as u16; @@ -1828,9 +1825,9 @@ mod audio { } } - fn draw_meters (meters: &[f32]) -> impl Content + use<'_> { - Tui::bg(Black, Fixed::X(2, Map::east(1, ||meters.iter(), |value, _index|{ - Fill::Y(RmsMeter(*value)) + fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { + Tui::bg(Black, w_exact(2, iter_east(1, ||meters.iter(), |value, _index|{ + h_full(RmsMeter(*value)) }))) } } @@ -1879,14 +1876,14 @@ mod audio { fn _todo_usize_stub_ (&self) -> usize { todo!() } fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } - pub fn per <'a, T: Content + 'a, U: TracksSizes<'a>> ( + pub fn per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Content + 'a { - Map::new(tracks, + ) -> impl Draw + 'a { + iter(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ let width = (x2 - x1) as u16; - map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg( + iter_east(x1 as u16, width, w_exact(width, Tui::fg_bg( track.color.lightest.rgb, track.color.base.rgb, callback(index, track))))}) @@ -1932,21 +1929,21 @@ mod audio { } } - pub fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( + pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Content + 'a { - per_track_top(tracks, move|index, track|Fill::Y(Align::y(callback(index, track)))) + ) -> impl Draw + 'a { + per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track)))) } - pub fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( + pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Content + 'a { - Align::x(Tui::bg(Reset, Map::new(tracks, + ) -> impl Draw + 'a { + origin_x(Tui::bg(Reset, iter(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ let width = (x2 - x1) as u16; - map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg( + iter_east(x1 as u16, width, w_exact(width, Tui::fg_bg( track.color.lightest.rgb, track.color.base.rgb, callback(index, track))))}))) @@ -2007,8 +2004,8 @@ mod audio { impl_has!(Clock: |self: Sequencer| self.clock); impl_has!(Vec: |self: Sequencer| self.midi_ins); impl_has!(Vec: |self: Sequencer| self.midi_outs); - impl_has!(Measure: |self: MidiEditor| self.size); - impl_has!(Measure: |self: PianoHorizontal| self.size); + impl_has!(Measure: |self: MidiEditor| self.size); + impl_has!(Measure: |self: PianoHorizontal| self.size); impl_default!(Sequencer: Self { clock: Clock::default(), play_clip: None, @@ -2224,11 +2221,15 @@ mod audio { } } } - impl Draw for MidiEditor { - fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } + impl Draw for MidiEditor { + fn draw(self, to: &mut Tui) -> Usually> { + self.tui().draw(to) + } } - impl Draw for PianoHorizontal { - fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } + impl Draw for PianoHorizontal { + fn draw(self, to: &mut Tui) -> Usually> { + self.tui().draw(to) + } } } @@ -2320,23 +2321,23 @@ mod audio { self.get_time_pos().overflowing_sub(1) .0.min(self.clip_length().saturating_sub(1)) } - pub fn clip_status (&self) -> impl Content + '_ { + pub fn clip_status (&self) -> impl Draw + '_ { let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { (clip.color, clip.name.clone(), clip.length, clip.looped) } else { (ItemTheme::G[64], String::new().into(), 0, false) }; - Fixed::X(20, col!( - Fill::X(Align::w(Bsp::e( + w_exact(20, south!( + w_full(origin_w(east( button_2("f2", "name ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), - Fill::X(Align::w(Bsp::e( + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), + w_full(origin_w(east( button_2("l", "ength ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), - Fill::X(Align::w(Bsp::e( + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), + w_full(origin_w(east( button_2("r", "epeat ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), )) } - pub fn edit_status (&self) -> impl Content + '_ { + pub fn edit_status (&self) -> impl Draw + '_ { let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { (clip.color, clip.length) } else { (ItemTheme::G[64], 0) }; @@ -2347,18 +2348,18 @@ mod audio { let note_name = format!("{:4}", note_pitch_to_name(note_pos)); let note_pos = format!("{:>3}", note_pos); let note_len = format!("{:>4}", self.get_note_len()); - Fixed::X(20, col!( - Fill::X(Align::w(Bsp::e( + w_exact(20, south!( + w_full(origin_w(east( button_2("t", "ime ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{length} /{time_zoom} +{time_pos} "))))))), - Fill::X(Align::w(Bsp::e( + w_full(origin_w(east( button_2("z", "lock ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{time_lock}"))))))), - Fill::X(Align::w(Bsp::e( + w_full(origin_w(east( button_2("x", "note ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{note_name} {note_pos} {note_len}"))))))), )) } @@ -2394,12 +2395,8 @@ mod audio { fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } } - impl Layout for MidiEditor { - fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } - } - - impl HasContent for MidiEditor { - fn content (&self) -> impl Content { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } + impl MidiEditor { + fn tui (&self) -> impl Draw { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } } @@ -2423,14 +2420,11 @@ mod audio { } } - impl Layout for PianoHorizontal { - fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } - } - impl HasContent for PianoHorizontal { - fn content (&self) -> impl Content { - Bsp::s( - Bsp::e(Fixed::X(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), - Bsp::e(self.keys(), self.size.of(Bsp::b(Fill::XY(self.notes()), Fill::XY(self.cursor())))), + impl PianoHorizontal { + fn tui (&self) -> impl Draw { + south( + east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), + east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(self.cursor())))), ) } } @@ -2507,12 +2501,12 @@ mod audio { } } - fn notes (&self) -> impl Content { + fn notes (&self) -> impl Draw { let time_start = self.get_time_start(); let note_lo = self.get_note_lo(); let note_hi = self.get_note_hi(); let buffer = self.buffer.clone(); - Thunk::new(move|to: &mut TuiOut|{ + Thunk::new(move|to: &mut Tui|{ let source = buffer.read().unwrap(); let XYWH(x0, y0, w, _h) = to.area(); //if h as usize != note_axis { @@ -2538,7 +2532,7 @@ mod audio { } }) } - fn cursor (&self) -> impl Content { + fn cursor (&self) -> impl Draw { let note_hi = self.get_note_hi(); let note_lo = self.get_note_lo(); let note_pos = self.get_note_pos(); @@ -2547,7 +2541,7 @@ mod audio { let time_start = self.get_time_start(); let time_zoom = self.get_time_zoom(); let style = Some(Style::default().fg(self.color.lightest.rgb)); - Thunk::new(move|to: &mut TuiOut|{ + Thunk::new(move|to: &mut Tui|{ let XYWH(x0, y0, w, _) = to.area(); for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { if note == note_pos { @@ -2569,7 +2563,7 @@ mod audio { } }) } - fn keys (&self) -> impl Content { + fn keys (&self) -> impl Draw { let state = self; let color = state.color; let note_lo = state.get_note_lo(); @@ -2578,7 +2572,7 @@ mod audio { let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); let off_style = Some(Style::default().fg(Tui::g(255))); let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); - Fill::Y(Fixed::X(self.keys_width, Thunk::new(move|to: &mut TuiOut|{ + h_full(w_exact(self.keys_width, Thunk::new(move|to: &mut Tui|{ let XYWH(x, y0, _w, _h) = to.area(); for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { to.blit(&to_key(note), x, screen_y, key_style); @@ -2593,8 +2587,8 @@ mod audio { } }))) } - fn timeline (&self) -> impl Content + '_ { - Fill::X(Fixed::Y(1, Thunk::new(move|to: &mut TuiOut|{ + fn timeline (&self) -> impl Draw + '_ { + w_full(h_exact(1, Thunk::new(move|to: &mut Tui|{ let XYWH(x, y, w, _h) = to.area(); let style = Some(Style::default().dim()); let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); @@ -2635,7 +2629,7 @@ mod audio { fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } /// Determine the required space to render the clip. fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { (clip.length / self.range.time_zoom().get(), 128) } - fn redraw (&self) { + fn redraw(self) { *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { let clip = clip.read().unwrap(); let buf_size = self.buffer_size(&clip); @@ -2671,9 +2665,9 @@ mod audio { self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] } } - impl HasContent for OctaveVertical { - fn content (&self) -> impl Content { - row!( + impl OctaveVertical { + fn tui (&self) -> impl Draw { + east!( Tui::fg_bg(self.color(0), self.color(1), "▙"), Tui::fg_bg(self.color(2), self.color(3), "▙"), Tui::fg_bg(self.color(4), self.color(5), "▌"), @@ -3107,8 +3101,8 @@ mod audio { Ok(()) } } - impl Draw for SampleAdd { - fn draw (&self, _to: &mut TuiOut) { + impl Draw for SampleAdd { + fn draw (self, _to: &mut Tui) -> Usually> { todo!() } } @@ -3129,9 +3123,9 @@ mod audio { } } - fn draw_viewer (sample: Option<&Arc>>) -> impl Content + use<'_> { + fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + use<'_> { let min_db = -64.0; - Thunk::new(move|to: &mut TuiOut|{ + Thunk::new(move|to: &mut Tui|{ let XYWH(x, y, width, height) = to.area(); let area = Rect { x, y, width, height }; if let Some(sample) = &sample { @@ -3216,7 +3210,7 @@ mod audio { } fn draw_sample ( - to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool + to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool ) -> Usually { let style = if focus { Style::default().green() } else { Style::default() }; if focus { @@ -3340,8 +3334,8 @@ mod audio { } } - impl Draw for Lv2 { - fn draw (&self, to: &mut TuiOut) { + impl Draw for Lv2 { + fn draw(self, to: &mut Tui) { let area = to.area(); let XYWH(x, y, _, height) = area; let mut width = 20u16; @@ -3373,7 +3367,7 @@ mod audio { } - fn draw_header (state: &Lv2, to: &mut TuiOut, x: u16, y: u16, w: u16) { + fn draw_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) { let style = Style::default().gray(); let label1 = format!(" {}", state.name); to.blit(&label1, x + 1, y, Some(style.white().bold())); @@ -3387,8 +3381,8 @@ mod audio { mod pool { use crate::*; - has_clips!(|self: Pool|self.clips); has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); + impl_has_clips!(|self: Pool|self.clips); impl_from!(Pool: |clip:&Arc>|{ let model = Self::default(); model.clips.write().unwrap().push(clip.clone()); @@ -3523,14 +3517,14 @@ mod pool { fn _color_random (&self) -> ItemColor { ItemColor::random() } } - impl<'a> HasContent for PoolView<'a> { - fn content (&self) -> impl Content { + impl<'a> PoolView<'a> { + fn tui (&self) -> impl Draw { let Self(pool) = self; //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); - //let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); + //let on_bg = |x|x;//below(Repeat(" "), Tui::bg(color.darkest.rgb, x)); //let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); //let height = pool.clips.read().unwrap().len() as u16; - Fixed::X(20, Fill::Y(Align::n(Map::new( + w_exact(20, h_full(origin_n(iter( ||pool.clips().clone().into_iter(), move|clip: Arc>, i: usize|{ let item_height = 1; @@ -3541,26 +3535,26 @@ mod pool { let fg = color.lightest.rgb; let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; let length = if false { String::default() } else { format!("{length} ") }; - Fixed::Y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( - Fill::X(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), - Fill::X(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), - Fill::X(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), - Fill::X(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), + h_exact(1, iter_south(item_offset, item_height, Tui::bg(bg, below!( + w_full(origin_w(Tui::fg(fg, Tui::bold(selected, name)))), + w_full(origin_e(Tui::fg(fg, Tui::bold(selected, length)))), + w_full(origin_w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), + w_full(origin_e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), )))) })))) } } - impl HasContent for ClipLength { - fn content (&self) -> impl Content { + impl ClipLength { + fn tui (&self) -> impl Draw { use ClipLengthFocus::*; let bars = ||self.bars_string(); let beats = ||self.beats_string(); let ticks = ||self.ticks_string(); match self.focus { - None => row!(" ", bars(), ".", beats(), ".", ticks()), - Some(Bar) => row!("[", bars(), "]", beats(), ".", ticks()), - Some(Beat) => row!(" ", bars(), "[", beats(), "]", ticks()), - Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()), + None => east!(" ", bars(), ".", beats(), ".", ticks()), + Some(Bar) => east!("[", bars(), "]", beats(), ".", ticks()), + Some(Beat) => east!(" ", bars(), "[", beats(), "]", ticks()), + Some(Tick) => east!(" ", bars(), ".", beats(), "[", ticks()), } } } @@ -3848,14 +3842,6 @@ mod config { Ok(map) } } - - // Each mode contains a view, so here we should be drawing it. - // I'm not sure what's going on with this code, though. - impl Draw for Mode { - fn draw (&self, _to: &mut TuiOut) { - //self.content().draw(to) - } - } } mod dialog { diff --git a/app/tek_struct.rs b/app/tek_struct.rs index 34d2277f..6af03ff8 100644 --- a/app/tek_struct.rs +++ b/app/tek_struct.rs @@ -31,7 +31,7 @@ use builder_pattern::Builder; /// Must not be dropped for the duration of the process pub jack: Jack<'static>, /// Display size - pub size: Measure, + pub size: Measure, /// Performance counter pub perf: PerfModel, /// Available view modes and input bindings @@ -468,9 +468,9 @@ use builder_pattern::Builder; /// TODO rename to "render_cache" or smth pub arranger: Arc>, /// Display size - pub size: Measure, + pub size: Measure, /// Display size of clips area - pub size_inner: Measure, + pub size_inner: Measure, /// Source of time #[cfg(feature = "clock")] pub clock: Clock, /// Allows one MIDI clip to be edited @@ -509,7 +509,7 @@ use builder_pattern::Builder; pub filter: String, pub index: usize, pub scroll: usize, - pub size: Measure, + pub size: Measure, } pub(crate) struct EntriesIterator<'a> { @@ -598,7 +598,7 @@ pub struct DeviceAudio<'a>(pub &'a mut Device); /// ``` pub struct MidiEditor { /// Size of editor on screen - pub size: Measure, + pub size: Measure, /// View mode and state of editor pub mode: PianoHorizontal, } @@ -613,7 +613,7 @@ pub struct MidiEditor { /// Buffer where the whole clip is rerendered on change pub buffer: Arc>, /// Size of actual notes area - pub size: Measure, + pub size: Measure, /// The display window pub range: MidiSelection, /// The note cursor @@ -808,7 +808,7 @@ pub struct PoolView<'a>(pub &'a Pool); /// Currently active modal, if any. pub mode: Option, /// Size of rendered sampler. - pub size: Measure, + pub size: Measure, /// Lowest note displayed. pub note_lo: AtomicUsize, /// Currently selected note. diff --git a/app/tek_trait.rs b/app/tek_trait.rs index 2ee87476..57fdde33 100644 --- a/app/tek_trait.rs +++ b/app/tek_trait.rs @@ -119,7 +119,7 @@ pub trait MidiRange: TimeRange + NoteRange {} /// can be clocked in microseconds with f64 without losing precision. pub trait TimeUnit: InteriorMutable {} -pub trait HasClipsSize { fn clips_size (&self) -> &Measure; } +pub trait HasClipsSize { fn clips_size (&self) -> &Measure; } pub trait HasMidiClip { fn clip (&self) -> Option>>; @@ -327,16 +327,16 @@ pub trait HasTracks: AsRef> + AsMut> { pub trait HasTrack: AsRefOpt + AsMutOpt { fn track (&self) -> Option<&Track> { self.as_ref_opt() } fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() } - #[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Content + 'a { + #[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw + 'a { self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) } - #[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Content + '_ { + #[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw + '_ { self.track().map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) } - #[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Content { + #[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw { self.track().map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) } - #[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Content { + #[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw { self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) } } @@ -379,7 +379,7 @@ pub trait HasPlayClip: HasClock { *self.reset_mut() = true; } - fn play_status (&self) -> impl Content { + fn play_status (&self) -> impl Draw { let (name, color): (Arc, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() { let MidiClip { ref name, color, .. } = *clip.read().unwrap(); (name.clone(), color) @@ -389,10 +389,10 @@ pub trait HasPlayClip: HasClock { let time: String = self.pulses_since_start_looped() .map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time))) .unwrap_or_else(||String::from(" ")).into(); - FieldV(color, "Now:", format!("{} {}", time, name)) + field_v(color, "Now:", format!("{} {}", time, name)) } - fn next_status (&self) -> impl Content { + fn next_status (&self) -> impl Draw { let mut time: Arc = String::from("--.-.--").into(); let mut name: Arc = String::from("").into(); let mut color = ItemTheme::G[64]; @@ -425,7 +425,7 @@ pub trait HasPlayClip: HasClock { name = "Stop".to_string().into(); } }; - FieldV(color, "Next:", format!("{} {}", time, name)) + field_v(color, "Next:", format!("{} {}", time, name)) } } @@ -489,7 +489,7 @@ pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { } } -pub trait MidiViewer: Measured + MidiRange + MidiPoint + Debug + Send + Sync { +pub trait MidiViewer: Measured + MidiRange + MidiPoint + Debug + Send + Sync { fn buffer_size (&self, clip: &MidiClip) -> (usize, usize); fn redraw (&self); fn clip (&self) -> &Option>>; @@ -557,20 +557,20 @@ pub trait MidiViewer: Measured + MidiRange + MidiPoint + Debug + Send + pub trait ClipsView: TracksView + ScenesView { fn view_scenes_clips <'a> (&'a self) - -> impl Content + 'a + -> impl Draw + 'a { - self.clips_size().of(Fill::XY(Bsp::a( - Fill::XY(Align::se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), - Thunk::new(|to: &mut TuiOut|for ( + self.clips_size().of(wh_full(above( + wh_full(origin_se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), + Thunk::new(|to: &mut Tui|for ( track_index, track, _, _ ) in self.tracks_with_sizes() { - to.place(&Fixed::X(track.width as u16, - Fill::Y(self.view_track_clips(track_index, track)))) + to.place(&w_exact(track.width as u16, + h_full(self.view_track_clips(track_index, track)))) })))) } - fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Content + 'a { - Thunk::new(move|to: &mut TuiOut|for ( + fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw + 'a { + Thunk::new(move|to: &mut Tui|for ( scene_index, scene, .. ) in self.scenes_with_sizes() { let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { @@ -609,14 +609,14 @@ pub trait ClipsView: TracksView + ScenesView { Self::H_SCENE as usize } as u16; - to.place(&Fixed::XY(w, y, Bsp::b( - Fill::XY(Outer(true, Style::default().fg(outline))), - Fill::XY(Bsp::b( - Bsp::b( - Tui::fg_bg(outline, bg, Fill::XY("")), - Fill::XY(Align::nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))), + to.place(&wh_exact(w, y, below( + wh_full(Outer(true, Style::default().fg(outline))), + wh_full(below( + below( + Tui::fg_bg(outline, bg, wh_full("")), + wh_full(origin_nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))), ), - Fill::XY(When::new(self.selection().track() == Some(track_index) + wh_full(when(self.selection().track() == Some(track_index) && self.selection().scene() == Some(scene_index) && self.is_editing(), self.editor()))))))); }) @@ -624,7 +624,7 @@ pub trait ClipsView: TracksView + ScenesView { } -pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured { +pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured { fn tracks_width_available (&self) -> u16 { (self.measure_width() as u16).saturating_sub(40) @@ -647,68 +647,68 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + M }) } - fn view_track_names (&self, theme: ItemTheme) -> impl Content { + fn view_track_names (&self, theme: ItemTheme) -> impl Draw { let track_count = self.tracks().len(); let scene_count = self.scenes().len(); let selected = self.selection(); - let button = Bsp::s( + let button = south( button_3("t", "rack ", format!("{}{track_count}", selected.track() .map(|track|format!("{track}/")).unwrap_or_default()), false), button_3("s", "cene ", format!("{}{scene_count}", selected.scene() .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); - let button_2 = Bsp::s( + let button_2 = south( button_2("T", "+", false), button_2("S", "+", false)); view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb, - Fixed::Y(2, Thunk::new(|to: &mut TuiOut|{ + h_exact(2, Thunk::new(|to: &mut Tui|{ for (index, track, x1, _x2) in self.tracks_with_sizes() { - to.place(&Push::X(x1 as u16, Fixed::X(track_width(index, track), + to.place(&x_push(x1 as u16, w_exact(track_width(index, track), Tui::bg(if selected.track() == Some(index) { track.color.light.rgb } else { track.color.base.rgb - }, Bsp::s(Fill::X(Align::nw(Bsp::e( + }, south(w_full(origin_nw(east( format!("·t{index:02} "), Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name)) ))), ""))) ));}})))) } - fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Content { + fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw { view_track_row_section(theme, - Bsp::s(Fill::X(Align::w(button_2("o", "utput", false))), - Thunk::new(|to: &mut TuiOut|for port in self.midi_outs().iter() { - to.place(&Fill::X(Align::w(port.port_name()))); + south(w_full(origin_w(button_2("o", "utput", false))), + Thunk::new(|to: &mut Tui|for port in self.midi_outs().iter() { + to.place(&w_full(origin_w(port.port_name()))); })), button_2("O", "+", false), - Tui::bg(theme.darker.rgb, Align::w(Thunk::new(|to: &mut TuiOut|{ + Tui::bg(theme.darker.rgb, origin_w(Thunk::new(|to: &mut Tui|{ for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Fixed::X(track_width(index, track), - Align::nw(Fill::Y(Map::south(1, ||track.sequencer.midi_outs.iter(), + to.place(&w_exact(track_width(index, track), + origin_nw(h_full(iter_south(1, ||track.sequencer.midi_outs.iter(), |port, index|Tui::fg(Rgb(255, 255, 255), - Fixed::Y(1, Tui::bg(track.color.dark.rgb, Fill::X(Align::w( + h_exact(1, Tui::bg(track.color.dark.rgb, w_full(origin_w( format!("·o{index:02} {}", port.port_name())))))))))));}})))) } - fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content { + fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw { let mut h = 0u16; for track in self.tracks().iter() { h = h.max(track.sequencer.midi_ins.len() as u16); } - let content = Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Fixed::XY(track_width(index, track), h + 1, - Align::nw(Bsp::s( + let content = Thunk::new(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&wh_exact(track_width(index, track), h + 1, + origin_nw(south( Tui::bg(track.color.base.rgb, - Fill::X(Align::w(row!( - Either::new(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "), - Either::new(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "), - Either::new(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "), + w_full(origin_w(east!( + either(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "), + either(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "), + either(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "), )))), - Map::south(1, ||track.sequencer.midi_ins.iter(), + iter_south(1, ||track.sequencer.midi_ins.iter(), |port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb, - Fill::X(Align::w(format!("·i{index:02} {}", port.port_name()))))))))); + w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))); }); view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), - Tui::bg(theme.darker.rgb, Align::w(content))) + Tui::bg(theme.darker.rgb, origin_w(content))) } } @@ -739,13 +739,13 @@ pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + }) } - fn view_scenes_names (&self) -> impl Content { - Fixed::X(20, Thunk::new(|to: &mut TuiOut|for (index, scene, ..) in self.scenes_with_sizes() { + fn view_scenes_names (&self) -> impl Draw { + w_exact(20, Thunk::new(|to: &mut Tui|for (index, scene, ..) in self.scenes_with_sizes() { to.place(&self.view_scene_name(index, scene)); })) } - fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Content + 'a { + fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw + 'a { let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() { 7 } else { @@ -756,13 +756,13 @@ pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + } else { scene.color.base.rgb }; - let a = Fill::X(Align::w(Bsp::e(format!("·s{index:02} "), + let a = w_full(origin_w(east(format!("·s{index:02} "), Tui::fg(Tui::g(255), Tui::bold(true, &scene.name))))); - let b = When::new(self.selection().scene() == Some(index) && self.is_editing(), - Fill::XY(Align::nw(Bsp::s( + let b = when(self.selection().scene() == Some(index) && self.is_editing(), + wh_full(origin_nw(south( self.editor().as_ref().map(|e|e.clip_status()), self.editor().as_ref().map(|e|e.edit_status()))))); - Fixed::XY(20, h, Tui::bg(bg, Align::nw(Bsp::s(a, b)))) + wh_exact(20, h, Tui::bg(bg, origin_nw(south(a, b)))) } } diff --git a/tengri b/tengri index 9dbf4fca..cf57f449 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 9dbf4fcab5f31a68e3d24c8f8f7fc866159e89f1 +Subproject commit cf57f44933c45507e8de072e32c284f20f12ac7a From 513b8354a30a44eb34284aa84f8e0bf15d9d2102 Mon Sep 17 00:00:00 2001 From: okay stopped screaming Date: Sat, 21 Mar 2026 22:54:29 +0200 Subject: [PATCH 2/7] wip: nromalize --- app/arrange.rs | 416 ++++++++++++++++++ app/browse.rs | 97 +++++ app/connect.rs | 306 +++++++++++++ app/device.rs | 0 app/mix.rs | 14 + app/plugin.rs | 28 ++ app/sample.rs | 104 +++++ app/sequence.rs | 596 ++++++++++++++++++++++++++ app/tek.rs | 599 +++++++++++++++++++++++++- app/tek_impls.rs | 347 --------------- app/tek_struct.rs | 1042 --------------------------------------------- app/tek_trait.rs | 919 --------------------------------------- app/tek_type.rs | 25 -- app/tick.rs | 168 ++++++++ 14 files changed, 2324 insertions(+), 2337 deletions(-) create mode 100644 app/arrange.rs create mode 100644 app/browse.rs create mode 100644 app/connect.rs create mode 100644 app/device.rs create mode 100644 app/mix.rs create mode 100644 app/plugin.rs create mode 100644 app/sample.rs create mode 100644 app/sequence.rs delete mode 100644 app/tek_struct.rs delete mode 100644 app/tek_trait.rs delete mode 100644 app/tek_type.rs create mode 100644 app/tick.rs diff --git a/app/arrange.rs b/app/arrange.rs new file mode 100644 index 00000000..0034912d --- /dev/null +++ b/app/arrange.rs @@ -0,0 +1,416 @@ + +/// Represents the current user selection in the arranger +#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { + #[default] + /// Nothing is selected + Nothing, + /// The whole mix is selected + Mix, + /// A MIDI input is selected. + Input(usize), + /// A MIDI output is selected. + Output(usize), + /// A scene is selected. + #[cfg(feature = "scene")] Scene(usize), + /// A track is selected. + #[cfg(feature = "track")] Track(usize), + /// A clip (track × scene) is selected. + #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, + /// A track's MIDI input connection is selected. + #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, + /// A track's MIDI output connection is selected. + #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, + /// A track device slot is selected. + #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, +} + +/// A scene consists of a set of clips to play together. +/// +/// ``` +/// let scene: tek::Scene = Default::default(); +/// let _ = scene.pulses(); +/// let _ = scene.is_playing(&[]); +/// ``` +#[derive(Debug, Default)] pub struct Scene { + /// Name of scene + pub name: Arc, + /// Identifying color of scene + pub color: ItemTheme, + /// Clips in scene, one per track + pub clips: Vec>>>, +} + +/// Arranger. +/// +/// ``` +/// let arranger = tek::Arrangement::default(); +/// ``` +#[derive(Default, Debug)] pub struct Arrangement { + /// Project name. + pub name: Arc, + /// Base color. + pub color: ItemTheme, + /// JACK client handle. + pub jack: Jack<'static>, + /// FIXME a render of the project arrangement, redrawn on update. + /// TODO rename to "render_cache" or smth + pub arranger: Arc>, + /// Display size + pub size: Measure, + /// Display size of clips area + pub size_inner: Measure, + /// Source of time + #[cfg(feature = "clock")] pub clock: Clock, + /// Allows one MIDI clip to be edited + #[cfg(feature = "editor")] pub editor: Option, + /// List of global midi inputs + #[cfg(feature = "port")] pub midi_ins: Vec, + /// List of global midi outputs + #[cfg(feature = "port")] pub midi_outs: Vec, + /// List of global audio inputs + #[cfg(feature = "port")] pub audio_ins: Vec, + /// List of global audio outputs + #[cfg(feature = "port")] pub audio_outs: Vec, + /// Selected UI element + #[cfg(feature = "select")] pub selection: Selection, + /// Last track number (to avoid duplicate port names) + #[cfg(feature = "track")] pub track_last: usize, + /// List of tracks + #[cfg(feature = "track")] pub tracks: Vec, + /// Scroll offset of tracks + #[cfg(feature = "track")] pub track_scroll: usize, + /// List of scenes + #[cfg(feature = "scene")] pub scenes: Vec, + /// Scroll offset of scenes + #[cfg(feature = "scene")] pub scene_scroll: usize, +} + +pub trait ClipsView: TracksView + ScenesView { + + fn view_scenes_clips <'a> (&'a self) + -> impl Draw + 'a + { + self.clips_size().of(wh_full(above( + wh_full(origin_se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), + Thunk::new(|to: &mut Tui|for ( + track_index, track, _, _ + ) in self.tracks_with_sizes() { + to.place(&w_exact(track.width as u16, + h_full(self.view_track_clips(track_index, track)))) + })))) + } + + fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw + 'a { + Thunk::new(move|to: &mut Tui|for ( + scene_index, scene, .. + ) in self.scenes_with_sizes() { + let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { + let clip = clip.read().unwrap(); + (format!(" ⏹ {}", &clip.name).into(), clip.color) + } else { + (" ⏹ -- ".into(), ItemTheme::G[32]) + }; + let fg = theme.lightest.rgb; + let mut outline = theme.base.rgb; + let bg = if self.selection().track() == Some(track_index) + && self.selection().scene() == Some(scene_index) + { + outline = theme.lighter.rgb; + theme.light.rgb + } else if self.selection().track() == Some(track_index) + || self.selection().scene() == Some(scene_index) + { + outline = theme.darkest.rgb; + theme.base.rgb + } else { + theme.dark.rgb + }; + let w = if self.selection().track() == Some(track_index) + && let Some(editor) = self.editor () + { + (editor.measure_width() as usize).max(24).max(track.width) + } else { + track.width + } as u16; + let y = if self.selection().scene() == Some(scene_index) + && let Some(editor) = self.editor () + { + (editor.measure_height() as usize).max(12) + } else { + Self::H_SCENE as usize + } as u16; + + to.place(&wh_exact(w, y, below( + wh_full(Outer(true, Style::default().fg(outline))), + wh_full(below( + below( + Tui::fg_bg(outline, bg, wh_full("")), + wh_full(origin_nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))), + ), + wh_full(when(self.selection().track() == Some(track_index) + && self.selection().scene() == Some(scene_index) + && self.is_editing(), self.editor()))))))); + }) + } + +} + +pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured { + + fn tracks_width_available (&self) -> u16 { + (self.measure_width() as u16).saturating_sub(40) + } + + /// Iterate over tracks with their corresponding sizes. + fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { + let _editor_width = self.editor().map(|e|e.measure_width()); + let _active_track = self.selection().track(); + let mut x = 0; + self.tracks().iter().enumerate().map_while(move |(index, track)|{ + let width = track.width.max(8); + if x + width < self.clips_size().w() as usize { + let data = (index, track, x, x + width); + x += width + Self::TRACK_SPACING; + Some(data) + } else { + None + } + }) + } + + fn view_track_names (&self, theme: ItemTheme) -> impl Draw { + let track_count = self.tracks().len(); + let scene_count = self.scenes().len(); + let selected = self.selection(); + let button = south( + button_3("t", "rack ", format!("{}{track_count}", selected.track() + .map(|track|format!("{track}/")).unwrap_or_default()), false), + button_3("s", "cene ", format!("{}{scene_count}", selected.scene() + .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); + let button_2 = south( + button_2("T", "+", false), + button_2("S", "+", false)); + view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb, + h_exact(2, Thunk::new(|to: &mut Tui|{ + for (index, track, x1, _x2) in self.tracks_with_sizes() { + to.place(&x_push(x1 as u16, w_exact(track_width(index, track), + Tui::bg(if selected.track() == Some(index) { + track.color.light.rgb + } else { + track.color.base.rgb + }, south(w_full(origin_nw(east( + format!("·t{index:02} "), + Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name)) + ))), ""))) ));}})))) + } + + fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw { + view_track_row_section(theme, + south(w_full(origin_w(button_2("o", "utput", false))), + Thunk::new(|to: &mut Tui|for port in self.midi_outs().iter() { + to.place(&w_full(origin_w(port.port_name()))); + })), + button_2("O", "+", false), + Tui::bg(theme.darker.rgb, origin_w(Thunk::new(|to: &mut Tui|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&w_exact(track_width(index, track), + origin_nw(h_full(iter_south(1, ||track.sequencer.midi_outs.iter(), + |port, index|Tui::fg(Rgb(255, 255, 255), + h_exact(1, Tui::bg(track.color.dark.rgb, w_full(origin_w( + format!("·o{index:02} {}", port.port_name())))))))))));}})))) + } + + fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw { + let mut h = 0u16; + for track in self.tracks().iter() { + h = h.max(track.sequencer.midi_ins.len() as u16); + } + let content = Thunk::new(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&wh_exact(track_width(index, track), h + 1, + origin_nw(south( + Tui::bg(track.color.base.rgb, + w_full(origin_w(east!( + either(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "), + either(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "), + either(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "), + )))), + iter_south(1, ||track.sequencer.midi_ins.iter(), + |port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb, + w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))); + }); + view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), + Tui::bg(theme.darker.rgb, origin_w(content))) + } + +} + +pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync { + /// Default scene height. + const H_SCENE: usize = 2; + /// Default editor height. + const H_EDITOR: usize = 15; + fn h_scenes (&self) -> u16; + fn w_side (&self) -> u16; + fn w_mid (&self) -> u16; + fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> { + let mut y = 0; + self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{ + let height = if self.selection().scene() == Some(s) && self.editor().is_some() { + 8 + } else { + Self::H_SCENE + }; + if y + height <= self.clips_size().h() as usize { + let data = (s, scene, y, y + height); + y += height; + Some(data) + } else { + None + } + }) + } + + fn view_scenes_names (&self) -> impl Draw { + w_exact(20, Thunk::new(|to: &mut Tui|for (index, scene, ..) in self.scenes_with_sizes() { + to.place(&self.view_scene_name(index, scene)); + })) + } + + fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw + 'a { + let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() { + 7 + } else { + Self::H_SCENE as u16 + }; + let bg = if self.selection().scene() == Some(index) { + scene.color.light.rgb + } else { + scene.color.base.rgb + }; + let a = w_full(origin_w(east(format!("·s{index:02} "), + Tui::fg(Tui::g(255), Tui::bold(true, &scene.name))))); + let b = when(self.selection().scene() == Some(index) && self.is_editing(), + wh_full(origin_nw(south( + self.editor().as_ref().map(|e|e.clip_status()), + self.editor().as_ref().map(|e|e.edit_status()))))); + wh_exact(20, h, Tui::bg(bg, origin_nw(south(a, b)))) + } + +} +pub trait HasSceneScroll: HasScenes { fn scene_scroll (&self) -> usize; } +pub trait HasTrackScroll: HasTracks { fn track_scroll (&self) -> usize; } +pub trait HasScene: AsRefOpt + AsMutOpt { + fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() } + fn scene (&self) -> Option<&Scene> { self.as_ref_opt() } +} +pub trait HasSelection: AsRef + AsMut { + fn selection (&self) -> &Selection { self.as_ref() } + fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } + /// Get the active track + #[cfg(feature = "track")] + fn selected_track (&self) -> Option<&Track> where Self: HasTracks { + let index = self.selection().track()?; + self.tracks().get(index) + } + /// Get a mutable reference to the active track + #[cfg(feature = "track")] + fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { + let index = self.selection().track()?; + self.tracks_mut().get_mut(index) + } + /// Get the active scene + #[cfg(feature = "scene")] + fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { + let index = self.selection().scene()?; + self.scenes().get(index) + } + /// Get a mutable reference to the active scene + #[cfg(feature = "scene")] + fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { + let index = self.selection().scene()?; + self.scenes_mut().get_mut(index) + } + /// Get the active clip + #[cfg(feature = "clip")] + fn selected_clip (&self) -> Option>> where Self: HasScenes + HasTracks { + self.selected_scene()?.clips.get(self.selection().track()?)?.clone() + } +} +pub trait HasScenes: AsRef> + AsMut> { + fn scenes (&self) -> &Vec { self.as_ref() } + fn scenes_mut (&mut self) -> &mut Vec { self.as_mut() } + /// Generate the default name for a new scene + fn scene_default_name (&self) -> Arc { format!("s{:3>}", self.scenes().len() + 1).into() } + fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) } + /// Add multiple scenes + fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks { + let scene_color_1 = ItemColor::random(); + let scene_color_2 = ItemColor::random(); + for i in 0..n { + let _ = self.scene_add(None, Some( + scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() + ))?; + } + Ok(()) + } + /// Add a scene + fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<(usize, &mut Scene)> where Self: HasTracks + { + let scene = Scene { + 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(ItemTheme::random), + }; + self.scenes_mut().push(scene); + let index = self.scenes().len() - 1; + Ok((index, &mut self.scenes_mut()[index])) + } +} +pub trait HasTracks: AsRef> + AsMut> { + fn tracks (&self) -> &Vec { self.as_ref() } + fn tracks_mut (&mut self) -> &mut Vec { self.as_mut() } + /// Run audio callbacks for every track and every device + fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { + for track in self.tracks_mut().iter_mut() { + if Control::Quit == Audio::process(&mut track.sequencer, client, scope) { + return Control::Quit + } + for device in track.devices.iter_mut() { + if Control::Quit == DeviceAudio(device).process(client, scope) { + return Control::Quit + } + } + } + Control::Continue + } + fn track_longest_name (&self) -> usize { self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) } + /// Stop all playing clips + fn tracks_stop_all (&mut self) { for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } } + /// Stop all playing clips + fn tracks_launch (&mut self, clips: Option>>>>) { + if let Some(clips) = clips { + for (clip, track) in clips.iter().zip(self.tracks_mut()) { track.sequencer.enqueue_next(clip.as_ref()); } + } else { + for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } + } + } + /// Spacing between tracks. + const TRACK_SPACING: usize = 0; +} +pub trait HasTrack: AsRefOpt + AsMutOpt { + fn track (&self) -> Option<&Track> { self.as_ref_opt() } + fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() } + #[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw + 'a { + self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) + } + #[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw + '_ { + self.track().map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) + } + #[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw { + self.track().map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) + } + #[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw { + self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) + } +} diff --git a/app/browse.rs b/app/browse.rs new file mode 100644 index 00000000..ccf7e37c --- /dev/null +++ b/app/browse.rs @@ -0,0 +1,97 @@ +/// Browses for files to load/save. +/// +/// ``` +/// let browse = tek::Browse::default(); +/// ``` +#[derive(Debug, Clone, Default, PartialEq)] pub struct Browse { + pub cwd: PathBuf, + pub dirs: Vec<(OsString, String)>, + pub files: Vec<(OsString, String)>, + pub filter: String, + pub index: usize, + pub scroll: usize, + pub size: Measure, +} + +pub(crate) struct EntriesIterator<'a> { + pub browser: &'a Browse, + pub offset: usize, + pub length: usize, + pub index: usize, +} + +#[derive(Clone, Debug)] pub enum BrowseTarget { + SaveProject, + LoadProject, + ImportSample(Arc>>), + ExportSample(Arc>>), + ImportClip(Arc>>), + ExportClip(Arc>>), +} + +/// A clip pool. +/// +/// ``` +/// let pool = tek::Pool::default(); +/// ``` +#[derive(Debug)] pub struct Pool { + pub visible: bool, + /// Selected clip + pub clip: AtomicUsize, + /// Mode switch + pub mode: Option, + /// Embedded file browse + #[cfg(feature = "browse")] pub browse: Option, + /// Collection of MIDI clips. + #[cfg(feature = "clip")] pub clips: Arc>>>>, + /// Collection of sound samples. + #[cfg(feature = "sampler")] pub samples: Arc>>>>, +} + +/// Displays and edits clip length. +#[derive(Clone, Debug, Default)] pub struct ClipLength { + /// Pulses per beat (quaver) + pub ppq: usize, + /// Beats per bar + pub bpb: usize, + /// Length of clip in pulses + pub pulses: usize, + /// Selected subdivision + pub focus: Option, +} + +/// Some sort of wrapper again? +pub struct PoolView<'a>(pub &'a Pool); + +// Commands supported by [Browse] +//#[derive(Debug, Clone, PartialEq)] +//pub enum BrowseCommand { + //Begin, + //Cancel, + //Confirm, + //Select(usize), + //Chdir(PathBuf), + //Filter(Arc), +//} + +/// Modes for clip pool +#[derive(Debug, Clone)] pub enum PoolMode { + /// Renaming a pattern + Rename(usize, Arc), + /// Editing the length of a pattern + Length(usize, usize, ClipLengthFocus), + /// Load clip from disk + Import(usize, Browse), + /// Save clip to disk + Export(usize, Browse), +} + +/// Focused field of `ClipLength` +#[derive(Copy, Clone, Debug)] pub enum ClipLengthFocus { + /// Editing the number of bars + Bar, + /// Editing the number of beats + Beat, + /// Editing the number of ticks + Tick, +} diff --git a/app/connect.rs b/app/connect.rs new file mode 100644 index 00000000..97bae48b --- /dev/null +++ b/app/connect.rs @@ -0,0 +1,306 @@ +use crate::*; + +/// Audio input port. +#[derive(Debug)] pub struct AudioInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +/// Audio output port. +#[derive(Debug)] pub struct AudioOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +/// MIDI input port. +#[derive(Debug)] pub struct MidiInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of currently held notes. + pub held: Arc>, + /// List of ports to connect to. + pub connections: Vec, +} + +/// MIDI output port. +#[derive(Debug)] pub struct MidiOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, + /// List of currently held notes. + pub held: Arc>, + /// Buffer + pub note_buffer: Vec, + /// Buffer + pub output_buffer: Vec>>, +} + +#[derive(Clone, Debug, PartialEq)] pub enum ConnectName { + /** Exact match */ + Exact(Arc), + /** Match regular expression */ + RegExp(Arc), +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { + One, + All +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { + Missing, + Disconnected, + Connected, + Mismatch, +} + +/// Port connection manager. +/// +/// ``` +/// let connect = tek::Connect::default(); +/// ``` +#[derive(Clone, Debug, Default)] pub struct Connect { + pub name: Option, + pub scope: Option, + pub status: Arc, Arc, ConnectStatus)>>>, + pub info: Arc, +} + +impl Connect { + pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) + -> Vec + { + let mut connections = vec![]; + for port in exact.iter() { connections.push(Self::exact(port)) } + for port in re.iter() { connections.push(Self::regexp(port)) } + for port in re_all.iter() { connections.push(Self::regexp_all(port)) } + connections + } + /// Connect to this exact port + pub fn exact (name: impl AsRef) -> Self { + let info = format!("=:{}", name.as_ref()).into(); + let name = Some(Exact(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp (name: impl AsRef) -> Self { + let info = format!("~:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp_all (name: impl AsRef) -> Self { + let info = format!("+:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn info (&self) -> Arc { + format!(" ({}) {} {}", { + let status = self.status.read().unwrap(); + let mut ok = 0; + for (_, _, state) in status.iter() { + if *state == Connected { + ok += 1 + } + } + format!("{ok}/{}", status.len()) + }, match self.scope { + None => "x", + Some(One) => " ", + Some(All) => "*", + }, match &self.name { + None => format!("x"), + Some(Exact(name)) => format!("= {name}"), + Some(RegExp(name)) => format!("~ {name}"), + }).into() + } +} + +impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl> RegisterPorts for J { + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiInput::new(self.jack(), name, connect) + } + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiOutput::new(self.jack(), name, connect) + } + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioInput::new(self.jack(), name, connect) + } + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioOutput::new(self.jack(), name, connect) + } +} + +/// May create new MIDI input ports. +pub trait AddMidiIn { + fn midi_in_add (&mut self) -> Usually<()>; +} + +/// May create new MIDI output ports. +pub trait AddMidiOut { + fn midi_out_add (&mut self) -> Usually<()>; +} + +pub trait RegisterPorts: HasJack<'static> { + /// Register a MIDI input port. + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register a MIDI output port. + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio input port. + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio output port. + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; +} + +pub trait JackPort: HasJack<'static> { + + type Port: PortSpec + Default; + + type Pair: PortSpec + Default; + + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized; + + fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { + jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) + .map_err(|e|e.into()) + } + + fn port_name (&self) -> &Arc; + + fn connections (&self) -> &[Connect]; + + fn port (&self) -> &Port; + + fn port_mut (&mut self) -> &mut Port; + + fn into_port (self) -> Port where Self: Sized; + + fn close (self) -> Usually<()> where Self: Sized { + let jack = self.jack().clone(); + Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) + } + + fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { + self.with_client(|c|c.ports(re_name, re_type, flags)) + } + + fn port_by_id (&self, id: u32) -> Option> { + self.with_client(|c|c.port_by_id(id)) + } + + fn port_by_name (&self, name: impl AsRef) -> Option> { + self.with_client(|c|c.port_by_name(name.as_ref())) + } + + fn connect_to_matching <'k> (&'k self) -> Usually<()> { + for connect in self.connections().iter() { + match &connect.name { + Some(Exact(name)) => { + *connect.status.write().unwrap() = self.connect_exact(name)?; + }, + Some(RegExp(re)) => { + *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; + }, + _ => {}, + }; + } + Ok(()) + } + + fn connect_exact <'k> (&'k self, name: &str) -> + Usually, Arc, ConnectStatus)>> + { + self.with_client(move|c|{ + let mut status = vec![]; + for port in c.ports(None, None, PortFlags::empty()).iter() { + if port.as_str() == &*name { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected { + break + } + } + } + } + Ok(status) + }) + } + + fn connect_regexp <'k> ( + &'k self, re: &str, scope: Option + ) -> Usually, Arc, ConnectStatus)>> { + self.with_client(move|c|{ + let mut status = vec![]; + let ports = c.ports(Some(&re), None, PortFlags::empty()); + for port in ports.iter() { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected && scope == Some(One) { + break + } + } + } + Ok(status) + }) + } + + /** Connect to a matching port by name. */ + fn connect_to_name (&self, name: impl AsRef) -> Usually { + self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { + self.connect_to_unowned(port) + } else { + Ok(Missing) + }) + } + + /** Connect to a matching port by reference. */ + fn connect_to_unowned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } + + /** Connect to an owned matching port by reference. */ + fn connect_to_owned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } + +} diff --git a/app/device.rs b/app/device.rs new file mode 100644 index 00000000..e69de29b diff --git a/app/mix.rs b/app/mix.rs new file mode 100644 index 00000000..63248378 --- /dev/null +++ b/app/mix.rs @@ -0,0 +1,14 @@ +#[derive(Debug, Default)] pub enum MeteringMode { + #[default] Rms, + Log10, +} + +#[derive(Debug, Default, Clone)] pub struct Log10Meter(pub f32); + +#[derive(Debug, Default, Clone)] pub struct RmsMeter(pub f32); + +#[derive(Debug, Default)] pub enum MixingMode { + #[default] Summing, + Average, +} + diff --git a/app/plugin.rs b/app/plugin.rs new file mode 100644 index 00000000..01d287f4 --- /dev/null +++ b/app/plugin.rs @@ -0,0 +1,28 @@ + +/// A LV2 plugin. +#[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 { + /// JACK client handle (needs to not be dropped for standalone mode to work). + pub jack: Jack<'static>, + pub name: Arc, + pub path: Option>, + pub selected: usize, + pub mapping: bool, + pub midi_ins: Vec>, + pub midi_outs: Vec>, + pub audio_ins: Vec>, + pub audio_outs: Vec>, + + pub lv2_world: livi::World, + pub lv2_instance: livi::Instance, + pub lv2_plugin: livi::Plugin, + pub lv2_features: Arc, + pub lv2_port_list: Vec, + pub lv2_input_buffer: Vec, + pub lv2_ui_thread: Option>, +} + +/// A LV2 plugin's X11 UI. +#[cfg(feature = "lv2_gui")] pub struct LV2PluginUI { + pub window: Option +} + diff --git a/app/sample.rs b/app/sample.rs new file mode 100644 index 00000000..773644cc --- /dev/null +++ b/app/sample.rs @@ -0,0 +1,104 @@ +/// Plays [Voice]s from [Sample]s. +/// +/// ``` +/// let sampler = tek::Sampler::default(); +/// ``` +#[derive(Debug, Default)] pub struct Sampler { + /// Name of sampler. + pub name: Arc, + /// Device color. + pub color: ItemTheme, + /// Sample currently being recorded. + pub recording: Option<(usize, Option>>)>, + /// Recording buffer. + pub buffer: Vec>, + /// Samples mapped to MIDI notes. + pub samples: SampleKit<128>, + /// Collection of currently playing instances of samples. + pub voices: Arc>>, + /// Samples that are not mapped to MIDI notes. + pub unmapped: Vec>>, + /// Sample currently being edited. + pub editing: Option>>, + /// How to mix the voices. + pub mixing_mode: MixingMode, + /// How to meter the inputs and outputs. + pub metering_mode: MeteringMode, + /// Fixed gain applied to all output. + pub output_gain: f32, + /// Currently active modal, if any. + pub mode: Option, + /// Size of rendered sampler. + pub size: Measure, + /// Lowest note displayed. + pub note_lo: AtomicUsize, + /// Currently selected note. + pub note_pt: AtomicUsize, + /// Selected note as row/col. + pub cursor: (AtomicUsize, AtomicUsize), + /// Audio input meters. + #[cfg(feature = "meter")] pub input_meters: Vec, + /// Audio input ports. Samples are recorded from here. + #[cfg(feature = "port")] pub audio_ins: Vec, + /// MIDI input port. Sampler are triggered from here. + #[cfg(feature = "port")] pub midi_in: Option, + /// Audio output ports. Voices are played into here. + #[cfg(feature = "port")] pub audio_outs: Vec, + /// Audio output meters. + #[cfg(feature = "meter")] pub output_meters: Vec, +} + +/// Collection of samples, one per slot, fixed number of slots. +/// +/// History: Separated to cleanly implement [Default]. +/// +/// ``` +/// let samples = tek::SampleKit([None, None, None, None]); +/// ``` +#[derive(Debug)] pub struct SampleKit( + pub [Option>>;N] +); + +/// A sound cut. +/// +/// ``` +/// let sample = tek::Sample::default(); +/// let sample = tek::Sample::new("test", 0, 0, vec![]); +/// ``` +#[derive(Default, Debug)] pub struct Sample { + pub name: Arc, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, + pub gain: f32, + pub color: ItemTheme, +} + +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} + +#[derive(Default, Debug)] pub struct SampleAdd { + pub exited: bool, + pub dir: PathBuf, + pub subdirs: Vec, + pub files: Vec, + pub cursor: usize, + pub offset: usize, + pub sample: Arc>, + pub voices: Arc>>, + pub _search: Option, +} + +#[derive(Debug)] pub enum SamplerMode { + // Load sample from path + Import(usize, Browse), +} + +pub type MidiSample = + (Option, Arc>); diff --git a/app/sequence.rs b/app/sequence.rs new file mode 100644 index 00000000..049b6dae --- /dev/null +++ b/app/sequence.rs @@ -0,0 +1,596 @@ + +/// Contains state for viewing and editing a clip. +/// +/// ``` +/// use std::sync::{Arc, RwLock}; +/// let clip = tek::MidiClip::stop_all(); +/// let mut editor = tek::MidiEditor { +/// mode: tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip)))), +/// size: Default::default(), +/// //keys: Default::default(), +/// }; +/// let _ = editor.put_note(true); +/// let _ = editor.put_note(false); +/// let _ = editor.clip_status(); +/// let _ = editor.edit_status(); +/// ``` +pub struct MidiEditor { + /// Size of editor on screen + pub size: Measure, + /// View mode and state of editor + pub mode: PianoHorizontal, +} + +/// A clip, rendered as a horizontal piano roll. +/// +/// ``` +/// let piano = tek::PianoHorizontal::default(); +/// ``` +#[derive(Clone, Default)] pub struct PianoHorizontal { + pub clip: Option>>, + /// Buffer where the whole clip is rerendered on change + pub buffer: Arc>, + /// Size of actual notes area + pub size: Measure, + /// The display window + pub range: MidiSelection, + /// The note cursor + pub point: MidiCursor, + /// The highlight color palette + pub color: ItemTheme, + /// Width of the keyboard + pub keys_width: u16, +} + +/// 12 piano keys, some highlighted. +/// +/// ``` +/// let keys = tek::OctaveVertical::default(); +/// ``` +#[derive(Copy, Clone)] pub struct OctaveVertical { + pub on: [bool; 12], + pub colors: [Color; 3] +} +/// A MIDI sequence. +/// +/// ``` +/// let clip = tek::MidiClip::default(); +/// ``` +#[derive(Debug, Clone, Default)] pub struct MidiClip { + pub uuid: uuid::Uuid, + /// Name of clip + pub name: Arc, + /// Temporal resolution in pulses per quarter note + pub ppq: usize, + /// Length of clip in pulses + pub length: usize, + /// Notes in clip + pub notes: MidiData, + /// Whether to loop the clip or play it once + pub looped: bool, + /// Start of loop + pub loop_start: usize, + /// Length of loop + pub loop_length: usize, + /// All notes are displayed with minimum length + pub percussive: bool, + /// Identifying color of clip + pub color: ItemTheme, +} + +/// Contains state for playing a clip +/// +/// ``` +/// let clip = tek::MidiClip::default(); +/// println!("Empty clip: {clip:?}"); +/// +/// let clip = tek::MidiClip::stop_all(); +/// println!("Panic clip: {clip:?}"); +/// +/// let mut clip = tek::MidiClip::new("clip", true, 1, None, None); +/// clip.set_length(96); +/// clip.toggle_loop(); +/// clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); +/// assert!(clip.contains_note_on(36.into(), 6, 18)); +/// assert_eq!(&clip.notes, &clip.duplicate().notes); +/// +/// let clip = std::sync::Arc::new(clip); +/// assert_eq!(clip.clone(), clip); +/// +/// let sequencer = tek::Sequencer::default(); +/// println!("{sequencer:?}"); +/// ``` +pub struct Sequencer { + /// State of clock and playhead + #[cfg(feature = "clock")] pub clock: Clock, + /// Start time and clip being played + #[cfg(feature = "clip")] pub play_clip: Option<(Moment, Option>>)>, + /// Start time and next clip + #[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option>>)>, + /// Record from MIDI ports to current sequence. + #[cfg(feature = "port")] pub midi_ins: Vec, + /// Play from current sequence to MIDI ports + #[cfg(feature = "port")] pub midi_outs: Vec, + /// Play input through output. + pub monitoring: bool, + /// Write input to sequence. + pub recording: bool, + /// Overdub input to sequence. + pub overdub: bool, + /// Send all notes off + pub reset: bool, // TODO?: after Some(nframes) + /// Notes currently held at input + pub notes_in: Arc>, + /// Notes currently held at output + pub notes_out: Arc>, + /// MIDI output buffer + pub note_buf: Vec, + /// MIDI output buffer + pub midi_buf: Vec>>, +} + +/// A track consists of a sequencer and zero or more devices chained after it. +/// +/// ``` +/// let track: tek::Track = Default::default(); +/// ``` +#[derive(Debug, Default)] pub struct Track { + /// Name of track + pub name: Arc, + /// Identifying color of track + pub color: ItemTheme, + /// Preferred width of track column + pub width: usize, + /// MIDI sequencer state + pub sequencer: Sequencer, + /// Device chain + pub devices: Vec, +} + + +pub trait HasPlayClip: HasClock { + + fn reset (&self) -> bool; + + fn reset_mut (&mut self) -> &mut bool; + + fn play_clip (&self) -> &Option<(Moment, Option>>)>; + + fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + + fn next_clip (&self) -> &Option<(Moment, Option>>)>; + + fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + + fn pulses_since_start (&self) -> Option { + if let Some((started, Some(_))) = self.play_clip().as_ref() { + let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); + return Some(elapsed) + } + None + } + + fn pulses_since_start_looped (&self) -> Option<(f64, f64)> { + if let Some((started, Some(clip))) = self.play_clip().as_ref() { + let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); + let length = clip.read().unwrap().length.max(1); // prevent div0 on empty clip + let times = (elapsed as usize / length) as f64; + let elapsed = (elapsed as usize % length) as f64; + return Some((times, elapsed)) + } + None + } + + fn enqueue_next (&mut self, clip: Option<&Arc>>) { + *self.next_clip_mut() = Some((self.clock().next_launch_instant(), clip.cloned())); + *self.reset_mut() = true; + } + + fn play_status (&self) -> impl Draw { + let (name, color): (Arc, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() { + let MidiClip { ref name, color, .. } = *clip.read().unwrap(); + (name.clone(), color) + } else { + ("".into(), Tui::g(64).into()) + }; + let time: String = self.pulses_since_start_looped() + .map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time))) + .unwrap_or_else(||String::from(" ")).into(); + field_v(color, "Now:", format!("{} {}", time, name)) + } + + fn next_status (&self) -> impl Draw { + let mut time: Arc = String::from("--.-.--").into(); + let mut name: Arc = String::from("").into(); + let mut color = ItemTheme::G[64]; + let clock = self.clock(); + if let Some((t, Some(clip))) = self.next_clip() { + let clip = clip.read().unwrap(); + name = clip.name.clone(); + color = clip.color.clone(); + time = { + let target = t.pulse.get(); + let current = clock.playhead.pulse.get(); + if target > current { + let remaining = target - current; + format!("-{:>}", clock.timebase.format_beats_1(remaining)) + } else { + String::new() + } + }.into() + } else if let Some((t, Some(clip))) = self.play_clip() { + let clip = clip.read().unwrap(); + if clip.looped { + name = clip.name.clone(); + color = clip.color.clone(); + let target = t.pulse.get() + clip.length as f64; + let current = clock.playhead.pulse.get(); + if target > current { + time = format!("-{:>}", clock.timebase.format_beats_0(target - current)).into() + } + } else { + name = "Stop".to_string().into(); + } + }; + field_v(color, "Next:", format!("{} {}", time, name)) + } +} + +pub trait MidiMonitor: HasMidiIns + HasMidiBuffers { + /// Input note flags. + fn notes_in (&self) -> &Arc>; + /// Current monitoring status. + fn monitoring (&self) -> bool; + /// Mutable monitoring status. + fn monitoring_mut (&mut self) -> &mut bool; + /// Enable or disable monitoring. + fn toggle_monitor (&mut self) { *self.monitoring_mut() = !self.monitoring(); } + /// Perform monitoring. + fn monitor (&mut self, _scope: &ProcessScope) { /* do nothing by default */ } +} + +pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { + fn recording (&self) -> bool; + fn recording_mut (&mut self) -> &mut bool; + fn toggle_record (&mut self) { + *self.recording_mut() = !self.recording(); + } + + fn overdub (&self) -> bool; + fn overdub_mut (&mut self) -> &mut bool; + fn toggle_overdub (&mut self) { + *self.overdub_mut() = !self.overdub(); + } + + fn record_clip ( + &mut self, + scope: &ProcessScope, + started: Moment, + clip: &Option>>, + ) { + if let Some(clip) = clip { + let sample0 = scope.last_frame_time() as usize; + let start = started.sample.get() as usize; + let _recording = self.recording(); + let timebase = self.clock().timebase().clone(); + let quant = self.clock().quant.get(); + let mut clip = clip.write().unwrap(); + let length = clip.length; + for input in self.midi_ins_mut().iter() { + for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) { + if let LiveEvent::Midi { message, .. } = event { + clip.record_event({ + let sample = (sample0 + sample - start) as f64; + let pulse = timebase.samples_to_pulse(sample); + let quantized = (pulse / quant).round() * quant; + quantized as usize % length + }, message); + } + } + } + } + } + + fn record_next (&mut self) { + // TODO switch to next clip and record into it + } +} + +pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize); + fn redraw (&self); + fn clip (&self) -> &Option>>; + fn clip_mut (&mut self) -> &mut Option>>; + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.redraw(); + } + /// Make sure cursor is within note range + fn autoscroll (&self) { + let note_pos = self.get_note_pos().min(127); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + if note_pos < note_lo { + self.note_lo().set(note_pos); + } else if note_pos > note_hi { + self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi)); + } + } + /// Make sure time range is within display + fn autozoom (&self) { + if self.time_lock().get() { + let time_len = self.get_time_len(); + let time_axis = self.get_time_axis(); + let time_zoom = self.get_time_zoom(); + loop { + let time_zoom = self.time_zoom().get(); + let time_area = time_axis * time_zoom; + if time_area > time_len { + let next_time_zoom = note_duration_prev(time_zoom); + if next_time_zoom <= 1 { + break + } + let next_time_area = time_axis * next_time_zoom; + if next_time_area >= time_len { + self.time_zoom().set(next_time_zoom); + } else { + break + } + } else if time_area < time_len { + let prev_time_zoom = note_duration_next(time_zoom); + if prev_time_zoom > 384 { + break + } + let prev_time_area = time_axis * prev_time_zoom; + if prev_time_area <= time_len { + self.time_zoom().set(prev_time_zoom); + } else { + break + } + } + } + if time_zoom != self.time_zoom().get() { + self.redraw() + } + } + //while time_len.div_ceil(time_zoom) > time_axis { + //println!("\r{time_len} {time_zoom} {time_axis}"); + //time_zoom = Note::next(time_zoom); + //} + //self.time_zoom().set(time_zoom); + } +} +pub type MidiData = + Vec>; +pub type ClipPool = + Vec>>; +pub type CollectedMidiInput<'a> = + Vec, MidiError>)>>; + +pub trait HasClips { + fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; + fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>; + fn add_clip (&self) -> (usize, Arc>) { + let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None))); + self.clips_mut().push(clip.clone()); + (self.clips().len() - 1, clip) + } +} +/// ``` +/// use tek::{*, tengri::*}; +/// +/// struct Test(Option); +/// impl_as_ref_opt!(MidiEditor: |self: Test|self.0.as_ref()); +/// impl_as_mut_opt!(MidiEditor: |self: Test|self.0.as_mut()); +/// +/// let mut host = Test(Some(MidiEditor::default())); +/// let _ = host.editor(); +/// let _ = host.editor_mut(); +/// let _ = host.is_editing(); +/// let _ = host.editor_w(); +/// let _ = host.editor_h(); +/// ``` +pub trait HasEditor: AsRefOpt + AsMutOpt { + fn editor (&self) -> Option<&MidiEditor> { self.as_ref_opt() } + fn editor_mut (&mut self) -> Option<&mut MidiEditor> { self.as_mut_opt() } + fn is_editing (&self) -> bool { self.editor().is_some() } + fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) as usize } + fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) as usize } +} +/// Trait for thing that may receive MIDI. +pub trait HasMidiIns { + fn midi_ins (&self) -> &Vec; + fn midi_ins_mut (&mut self) -> &mut Vec; + /// Collect MIDI input from app ports (TODO preallocate large buffers) + fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { + self.midi_ins().iter() + .map(|port|port.port().iter(scope) + .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) + .collect::>()) + .collect::>() + } + fn midi_ins_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_ins().iter().enumerate().map(move|(i, input)|{ + let height = 1 + input.connections().len(); + let data = (i, input.port_name(), input.connections(), y, y + height); + y += height; + data + }) + } +} +/// Trait for thing that may output MIDI. +pub trait HasMidiOuts { + fn midi_outs (&self) -> &Vec; + fn midi_outs_mut (&mut self) -> &mut Vec; + fn midi_outs_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_outs().iter().enumerate().map(move|(i, output)|{ + let height = 1 + output.connections().len(); + let data = (i, output.port_name(), output.connections(), y, y + height); + y += height; + data + }) + } + fn midi_outs_emit (&mut self, scope: &ProcessScope) { + for port in self.midi_outs_mut().iter_mut() { + port.buffer_emit(scope) + } + } +} +pub trait HasMidiClip { + fn clip (&self) -> Option>>; +} +pub trait HasSequencer: AsRef + AsMut { + fn sequencer_mut (&mut self) -> &mut Sequencer { self.as_mut() } + fn sequencer (&self) -> &Sequencer { self.as_ref() } +} +pub trait HasMidiBuffers { + fn note_buf_mut (&mut self) -> &mut Vec; + fn midi_buf_mut (&mut self) -> &mut Vec>>; +} + + +pub trait NotePoint { + fn note_len (&self) -> &AtomicUsize; + /// Get the current length of the note cursor. + fn get_note_len (&self) -> usize { + self.note_len().load(Relaxed) + } + /// Set the length of the note cursor, returning the previous value. + fn set_note_len (&self, x: usize) -> usize { + self.note_len().swap(x, Relaxed) + } + + fn note_pos (&self) -> &AtomicUsize; + /// Get the current pitch of the note cursor. + fn get_note_pos (&self) -> usize { + self.note_pos().load(Relaxed).min(127) + } + /// Set the current pitch fo the note cursor, returning the previous value. + fn set_note_pos (&self, x: usize) -> usize { + self.note_pos().swap(x.min(127), Relaxed) + } +} + +pub trait TimePoint { + fn time_pos (&self) -> &AtomicUsize; + /// Get the current time position of the note cursor. + fn get_time_pos (&self) -> usize { + self.time_pos().load(Relaxed) + } + /// Set the current time position of the note cursor, returning the previous value. + fn set_time_pos (&self, x: usize) -> usize { + self.time_pos().swap(x, Relaxed) + } +} + +pub trait MidiPoint: NotePoint + TimePoint { + /// Get the current end of the note cursor. + fn get_note_end (&self) -> usize { + self.get_time_pos() + self.get_note_len() + } +} + +pub trait TimeRange { + fn time_len (&self) -> &AtomicUsize; + fn get_time_len (&self) -> usize { + self.time_len().load(Ordering::Relaxed) + } + fn time_zoom (&self) -> &AtomicUsize; + fn get_time_zoom (&self) -> usize { + self.time_zoom().load(Ordering::Relaxed) + } + fn set_time_zoom (&self, value: usize) -> usize { + self.time_zoom().swap(value, Ordering::Relaxed) + } + fn time_lock (&self) -> &AtomicBool; + fn get_time_lock (&self) -> bool { + self.time_lock().load(Ordering::Relaxed) + } + fn set_time_lock (&self, value: bool) -> bool { + self.time_lock().swap(value, Ordering::Relaxed) + } + fn time_start (&self) -> &AtomicUsize; + fn get_time_start (&self) -> usize { + self.time_start().load(Ordering::Relaxed) + } + fn set_time_start (&self, value: usize) -> usize { + self.time_start().swap(value, Ordering::Relaxed) + } + fn time_axis (&self) -> &AtomicUsize; + fn get_time_axis (&self) -> usize { + self.time_axis().load(Ordering::Relaxed) + } + fn get_time_end (&self) -> usize { + self.time_start().get() + self.time_axis().get() * self.time_zoom().get() + } +} + +pub trait NoteRange { + fn note_lo (&self) -> &AtomicUsize; + fn get_note_lo (&self) -> usize { + self.note_lo().load(Ordering::Relaxed) + } + fn set_note_lo (&self, x: usize) -> usize { + self.note_lo().swap(x, Ordering::Relaxed) + } + fn note_axis (&self) -> &AtomicUsize; + fn get_note_axis (&self) -> usize { + self.note_axis().load(Ordering::Relaxed) + } + fn get_note_hi (&self) -> usize { + (self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127) + } +} + +pub trait MidiRange: TimeRange + NoteRange {} + +/// +/// ``` +/// let _ = tek::MidiCursor::default(); +/// ``` +#[derive(Debug, Clone)] pub struct MidiCursor { + /// Time coordinate of cursor + pub time_pos: Arc, + /// Note coordinate of cursor + pub note_pos: Arc, + /// Length of note that will be inserted, in pulses + pub note_len: Arc, +} + +/// +/// ``` +/// use tek::{TimeRange, NoteRange}; +/// let model = tek::MidiSelection::from((1, false)); +/// +/// let _ = model.get_time_len(); +/// let _ = model.get_time_zoom(); +/// let _ = model.get_time_lock(); +/// let _ = model.get_time_start(); +/// let _ = model.get_time_axis(); +/// let _ = model.get_time_end(); +/// +/// let _ = model.get_note_lo(); +/// let _ = model.get_note_axis(); +/// let _ = model.get_note_hi(); +/// ``` +#[derive(Debug, Clone, Default)] pub struct MidiSelection { + pub time_len: Arc, + /// Length of visible time axis + pub time_axis: Arc, + /// Earliest time displayed + pub time_start: Arc, + /// Time step + pub time_zoom: Arc, + /// Auto rezoom to fit in time axis + pub time_lock: Arc, + /// Length of visible note axis + pub note_axis: Arc, + // Lowest note displayed + pub note_lo: Arc, +} diff --git a/app/tek.rs b/app/tek.rs index 828827f1..dc2c3169 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -4,10 +4,18 @@ impl_trait_in_assoc_type, trait_alias, type_alias_impl_trait, type_changing_struct_update )] -mod tek_struct; pub use self::tek_struct::*; -mod tek_trait; pub use self::tek_trait::*; -mod tek_type; pub use self::tek_type::*; -mod tek_impls; +pub mod arrange; +pub mod browse; +pub mod connect; +pub mod device; +pub mod mix; +pub mod plugin; +pub mod sample; +pub mod sequence; +pub mod tick; + +use clap::{self, Parser, Subcommand}; +use builder_pattern::Builder; extern crate xdg; pub(crate) use ::xdg::BaseDirectories; @@ -61,6 +69,7 @@ pub(crate) use tengri::{ ops::{Add, Sub, Mul, Div, Rem}, path::{Path, PathBuf}, sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, + time::Duration, thread::{spawn, JoinHandle}, }, }; @@ -1215,3 +1224,585 @@ pub(crate) const HEADER: &'static str = r#" ~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ █ term█▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~ ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; + +/// Total state +/// +/// ``` +/// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; +/// let mut app = tek::App::default(); +/// let _ = app.scene_add(None, None).unwrap(); +/// let _ = app.update_clock(); +/// app.project.editor = Some(Default::default()); +/// //let _: Vec<_> = app.project.inputs_with_sizes().collect(); +/// //let _: Vec<_> = app.project.outputs_with_sizes().collect(); +/// let _: Vec<_> = app.project.tracks_with_sizes().collect(); +/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); +/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); +/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); +/// let _ = app.project.w(); +/// //let _ = app.project.w_sidebar(); +/// //let _ = app.project.w_tracks_area(); +/// let _ = app.project.h(); +/// //let _ = app.project.h_tracks_area(); +/// //let _ = app.project.h_inputs(); +/// //let _ = app.project.h_outputs(); +/// let _ = app.project.h_scenes(); +/// ``` +#[derive(Default, Debug)] pub struct App { + /// Base color. + pub color: ItemTheme, + /// Must not be dropped for the duration of the process + pub jack: Jack<'static>, + /// Display size + pub size: Measure, + /// Performance counter + pub perf: PerfModel, + /// Available view modes and input bindings + pub config: Config, + /// Currently selected mode + pub mode: Arc>>, + /// Undo history + pub history: Vec<(AppCommand, Option)>, + /// Dialog overlay + pub dialog: Dialog, + /// Contains all recently created clips. + pub pool: Pool, + /// Contains the currently edited musical arrangement + pub project: Arrangement, + /// Error, if any + pub error: Arc>>> +} + +/// Configuration: mode, view, and bind definitions. +/// +/// ``` +/// let config = tek::Config::default(); +/// ``` +/// +/// ``` +/// // Some dizzle. +/// // What indentation to use here lol? +/// let source = stringify!((mode :menu (name Menu) +/// (info Mode selector.) (keys :axis/y :confirm) +/// (view (bg (g 0) (bsp/s :ports/out +/// (bsp/n :ports/in +/// (bg (g 30) (bsp/s (fixed/y 7 :logo) +/// (fill :dialog/menu))))))))); +/// // Add this definition to the config and try to load it. +/// // A "mode" is basically a state machine +/// // with associated input and output definitions. +/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap(); +/// ``` +#[derive(Default, Debug)] pub struct Config { + /// XDG base directories of running user. + pub dirs: BaseDirectories, + /// Active collection of interaction modes. + pub modes: Modes, + /// Active collection of event bindings. + pub binds: Binds, + /// Active collection of view definitions. + pub views: Views, +} + +/// Group of view and keys definitions. +/// +/// ``` +/// let mode = tek::Mode::>::default(); +/// ``` +#[derive(Default, Debug)] pub struct Mode { + pub path: PathBuf, + pub name: Vec, + pub info: Vec, + pub view: Vec, + pub keys: Vec, + pub modes: Modes, +} + +/// An map of input events (e.g. [TuiEvent]) to [Binding]s. +/// +/// ``` +/// let lang = "(@x (nop)) (@y (nop) (nop))"; +/// let bind = tek::Bind::>::load(&lang).unwrap(); +/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1)); +/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2)); +/// ``` +#[derive(Debug)] pub struct Bind( + /// Map of each event (e.g. key combination) to + /// all command expressions bound to it by + /// all loaded input layers. + pub BTreeMap>> +); + +/// A sequence of zero or more commands (e.g. [AppCommand]), +/// optionally filtered by [Condition] to form layers. +/// +/// ``` +/// //FIXME: Why does it overflow? +/// //let binding: Binding<()> = tek::Binding { ..Default::default() }; +/// ``` +#[derive(Debug, Clone)] pub struct Binding { + pub commands: Arc<[C]>, + pub condition: Option, + pub description: Option>, + pub source: Option>, +} + +/// Condition that must evaluate to true in order to enable an input layer. +/// +/// ``` +/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true}))); +/// ``` +#[derive(Clone)] pub struct Condition( + pub Arcbool + Send + Sync>> +); + +/// List of menu items. +/// +/// ``` +/// let items: tek::MenuItems = Default::default(); +/// ``` +#[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems( + pub Arc<[MenuItem]> +); + +/// An item of a menu. +/// +/// ``` +/// let item: tek::MenuItem = Default::default(); +/// ``` +#[derive(Clone)] pub struct MenuItem( + /// Label + pub Arc, + /// Callback + pub ArcUsually<()> + Send + Sync>> +); + +/// The command-line interface descriptor. +/// +/// ``` +/// let cli: tek::Cli = Default::default(); +/// +/// use clap::CommandFactory; +/// tek::Cli::command().debug_assert(); +/// ``` +#[derive(Parser)] +#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] +#[derive(Debug, Default)] pub struct Cli { + /// Pre-defined configuration modes. + /// + /// TODO: Replace these with scripted configurations. + #[command(subcommand)] pub action: Action, +} + +/// Application modes that can be passed to the mommand line interface. +/// +/// ``` +/// let action: tek::Action = Default::default(); +/// ``` +#[derive(Debug, Clone, Subcommand, Default)] pub enum Action { + /// Continue where you left off + #[default] Resume, + /// Run headlessly in current session. + Headless, + /// Show status of current session. + Status, + /// List known sessions. + List, + /// Continue work in a copy of the current session. + Fork, + /// Create a new empty session. + New { + /// Name of JACK client + #[arg(short='n', long)] name: Option, + /// Whether to attempt to become transport master + #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, + /// Whether to sync to external transport master + #[arg(short='y', long, default_value_t = true)] sync_follow: bool, + /// Initial tempo in beats per minute + #[arg(short='b', long, default_value = None)] bpm: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short='c', long, default_value_t = true)] show_clock: bool, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='I', long)] midi_from: Vec, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='i', long)] midi_from_re: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='O', long)] midi_to: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='o', long)] midi_to_re: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] left_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] right_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] left_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] right_to: Vec, + /// Tracks to create + #[arg(short='t', long)] tracks: Option, + /// Scenes to create + #[arg(short='s', long)] scenes: Option, + }, + /// Import media as new session. + Import, + /// Show configuration. + Config, + /// Show version. + Version, +} + +pub type SceneWith<'a, T> = + (usize, &'a Scene, usize, usize, T); + +/// Collection of interaction modes. +pub type Modes = Arc, Arc>>>>>; + +/// Collection of input bindings. +pub type Binds = Arc, Bind>>>>; + +/// Collection of view definitions. +pub type Views = Arc, Arc>>>; + +pub trait HasClipsSize { fn clips_size (&self) -> &Measure; } + +pub trait HasDevices: AsRef> + AsMut> { + fn devices (&self) -> &Vec { self.as_ref() } + fn devices_mut (&mut self) -> &mut Vec { self.as_mut() } +} +pub trait HasWidth { + const MIN_WIDTH: usize; + /// Increment track width. + fn width_inc (&mut self); + /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. + fn width_dec (&mut self); +} + +/// A control axis. +/// +/// ``` +/// let axis = tek::ControlAxis::X; +/// ``` +#[derive(Debug, Copy, Clone)] pub enum ControlAxis { + X, Y, Z, I +} + +/// Various possible dialog modes. +/// +/// ``` +/// let dialog: tek::Dialog = Default::default(); +/// ``` +#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { + #[default] None, + Help(usize), + Menu(usize, MenuItems), + Device(usize), + Message(Arc), + Browse(BrowseTarget, Arc), + Options, +} + +/// A device that can be plugged into the chain. +/// +/// ``` +/// let device = tek::Device::default(); +/// ``` +#[derive(Debug, Default)] pub enum Device { + #[default] + Bypass, + Mute, + #[cfg(feature = "sampler")] + Sampler(Sampler), + #[cfg(feature = "lv2")] // TODO + Lv2(Lv2), + #[cfg(feature = "vst2")] // TODO + Vst2, + #[cfg(feature = "vst3")] // TODO + Vst3, + #[cfg(feature = "clap")] // TODO + Clap, + #[cfg(feature = "sf2")] // TODO + Sf2, +} + +/// Some sort of wrapper? +pub struct DeviceAudio<'a>(pub &'a mut Device); + + /// Command-line configuration. + #[cfg(feature = "cli")] impl Cli { + pub fn run (&self) -> Usually<()> { + if let Action::Version = self.action { + return Ok(tek_show_version()) + } + + let mut config = Config::new(None); + config.init()?; + + if let Action::Config = self.action { + tek_print_config(&config); + } else if let Action::List = self.action { + todo!("list sessions") + } else if let Action::Resume = self.action { + todo!("resume session") + } else if let Action::New { + name, bpm, tracks, scenes, sync_lead, sync_follow, + midi_from, midi_from_re, midi_to, midi_to_re, + left_from, right_from, left_to, right_to, .. + } = &self.action { + + // Connect to JACK + let name = name.as_ref().map_or("tek", |x|x.as_str()); + let jack = Jack::new(&name)?; + + // TODO: Collect audio IO: + let empty = &[] as &[&str]; + let left_froms = Connect::collect(&left_from, empty, empty); + let left_tos = Connect::collect(&left_to, empty, empty); + let right_froms = Connect::collect(&right_from, empty, empty); + let right_tos = Connect::collect(&right_to, empty, empty); + let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; + let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; + + // Create initial project: + let clock = Clock::new(&jack, *bpm)?; + let mut project = Arrangement::new( + &jack, + None, + clock, + vec![], + vec![], + Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() + .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) + .collect::, _>>()?, + Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() + .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) + .collect::, _>>()? + ); + project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; + project.scenes_add(scenes.unwrap_or(0))?; + + if matches!(self.action, Action::Status) { + // Show status and exit + tek_print_status(&project); + return Ok(()) + } + + // Initialize the app state + let app = tek(&jack, project, config, ":menu"); + if matches!(self.action, Action::Headless) { + // TODO: Headless mode (daemon + client over IPC, then over network...) + println!("todo headless"); + return Ok(()) + } + + // Run the [Tui] and [Jack] threads with the [App] state. + Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ + + // Between jack init and app's first cycle: + + jack.sync_lead(*sync_lead, |mut state|{ + let clock = app.clock(); + clock.playhead.update_from_sample(state.position.frame() as f64); + state.position.bbt = Some(clock.bbt()); + state.position + })?; + + jack.sync_follow(*sync_follow)?; + + // FIXME: They don't work properly. + + Ok(app) + + })?)?; + } + Ok(()) + } + } + + impl Config { + const CONFIG_DIR: &'static str = "tek"; + const CONFIG_SUB: &'static str = "v0"; + const CONFIG: &'static str = "tek.edn"; + const DEFAULTS: &'static str = include_str!("./tek.edn"); + /// Create a new app configuration from a set of XDG base directories, + pub fn new (dirs: Option) -> Self { + let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); + let dirs = dirs.unwrap_or_else(default); + Self { dirs, ..Default::default() } + } + /// Write initial contents of configuration. + pub fn init (&mut self) -> Usually<()> { + self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ + cfgs.add(&dsl)?; + Ok(()) + })?; + Ok(()) + } + /// Write initial contents of a configuration file. + pub fn init_one ( + &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> + ) -> Usually<()> { + if self.dirs.find_config_file(path).is_none() { + //println!("Creating {path:?}"); + std::fs::write(self.dirs.place_config_file(path)?, defaults)?; + } + Ok(if let Some(path) = self.dirs.find_config_file(path) { + //println!("Loading {path:?}"); + let src = std::fs::read_to_string(&path)?; + src.as_str().each(move|item|each(self, item))?; + } else { + return Err(format!("{path}: not found").into()) + }) + } + /// Add statements to configuration from [Dsl] source. + pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { + dsl.each(|item|self.add_one(item))?; + Ok(self) + } + fn add_one (&self, item: impl Language) -> Usually<()> { + if let Some(expr) = item.expr()? { + let head = expr.head()?; + let tail = expr.tail()?; + let name = tail.head()?; + let body = tail.tail()?; + //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); + match head { + Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, + Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, + Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, + _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) + } + Ok(()) + } else { + return Err(format!("Config::load: expected expr, got: {item:?}").into()) + } + } + pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { + self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() + } + } + + impl Mode> { + /// Add a definition to the mode. + /// + /// Supported definitions: + /// + /// - (name ...) -> name + /// - (info ...) -> description + /// - (keys ...) -> key bindings + /// - (mode ...) -> submode + /// - ... -> view + /// + /// ``` + /// let mut mode: tek::Mode> = Default::default(); + /// mode.add("(name hello)").unwrap(); + /// ``` + pub fn add (&mut self, dsl: impl Language) -> Usually<()> { + Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { + //println!("Mode::add: {head} {:?}", expr.tail()); + let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + match head { + "name" => self.add_name(tail)?, + "info" => self.add_info(tail)?, + "keys" => self.add_keys(tail)?, + "mode" => self.add_mode(tail)?, + _ => self.add_view(tail)?, + }; + } else if let Ok(Some(word)) = dsl.word() { + self.add_view(word); + } else { + return Err(format!("Mode::add: unexpected: {dsl:?}").into()); + }) + + //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) + //.word(|word|self.add_view(word)) + //.expr(|expr|expr.head(|head|{ + ////println!("Mode::add: {head} {:?}", expr.tail()); + //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + //match head { + //"name" => self.add_name(tail), + //"info" => self.add_info(tail), + //"keys" => self.add_keys(tail)?, + //"mode" => self.add_mode(tail)?, + //_ => self.add_view(tail), + //}; + //})) + } + + fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.name.push(src.into()))) + } + fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.info.push(src.into()))) + } + fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.view.push(src.into()))) + } + fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) + } + fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(if let Some(id) = dsl.head()? { + load_mode(&self.modes, &id, &dsl.tail())?; + } else { + return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); + })) + } + } + + impl Bind { + /// Create a new event map + pub fn new () -> Self { + Default::default() + } + /// Add a binding to an owned event map. + pub fn def (mut self, event: E, binding: Binding) -> Self { + self.add(event, binding); + self + } + /// Add a binding to an event map. + pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { + if !self.0.contains_key(&event) { + self.0.insert(event.clone(), Default::default()); + } + self.0.get_mut(&event).unwrap().push(binding); + self + } + /// Return the binding(s) that correspond to an event. + pub fn query (&self, event: &E) -> Option<&[Binding]> { + self.0.get(event).map(|x|x.as_slice()) + } + /// Return the first binding that corresponds to an event, considering conditions. + pub fn dispatch (&self, event: &E) -> Option<&Binding> { + self.query(event) + .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) + .flatten() + } + } + + impl Bind> { + pub fn load (lang: &impl Language) -> Usually { + let mut map = Bind::new(); + lang.each(|item|if item.expr().head() == Ok(Some("see")) { + // TODO + Ok(()) + } else if let Ok(Some(_word)) = item.expr().head().word() { + if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { + map.add(key, Binding { + commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), + condition: None, + description: None, + source: None + }); + Ok(()) + } else if Some(":char") == item.expr()?.head()? { + // TODO + return Ok(()) + } else { + return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) + } + } else { + return Err(format!("Config::load_bind: unexpected: {item:?}").into()) + })?; + Ok(map) + } + } diff --git a/app/tek_impls.rs b/app/tek_impls.rs index b6b498cf..3e245cfe 100644 --- a/app/tek_impls.rs +++ b/app/tek_impls.rs @@ -941,55 +941,6 @@ mod clock { }); } -impl Connect { - pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) - -> Vec - { - let mut connections = vec![]; - for port in exact.iter() { connections.push(Self::exact(port)) } - for port in re.iter() { connections.push(Self::regexp(port)) } - for port in re_all.iter() { connections.push(Self::regexp_all(port)) } - connections - } - /// Connect to this exact port - pub fn exact (name: impl AsRef) -> Self { - let info = format!("=:{}", name.as_ref()).into(); - let name = Some(Exact(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp (name: impl AsRef) -> Self { - let info = format!("~:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp_all (name: impl AsRef) -> Self { - let info = format!("+:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn info (&self) -> Arc { - format!(" ({}) {} {}", { - let status = self.status.read().unwrap(); - let mut ok = 0; - for (_, _, state) in status.iter() { - if *state == Connected { - ok += 1 - } - } - format!("{ok}/{}", status.len()) - }, match self.scope { - None => "x", - Some(One) => " ", - Some(All) => "*", - }, match &self.name { - None => format!("x"), - Some(Exact(name)) => format!("= {name}"), - Some(RegExp(name)) => format!("~ {name}"), - }).into() - } -} - - impl Selection { pub fn describe ( &self, @@ -1118,26 +1069,8 @@ impl InteriorMutable for AtomicUsize { fn set (&self, value: usize) -> us impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } impl AsRef> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } } impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.size_inner } } -impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl> RegisterPorts for J { - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiInput::new(self.jack(), name, connect) - } - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiOutput::new(self.jack(), name, connect) - } - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioInput::new(self.jack(), name, connect) - } - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioOutput::new(self.jack(), name, connect) - } -} #[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock); @@ -3562,286 +3495,6 @@ mod pool { mod config { use crate::*; - - /// Command-line configuration. - #[cfg(feature = "cli")] impl Cli { - pub fn run (&self) -> Usually<()> { - if let Action::Version = self.action { - return Ok(tek_show_version()) - } - - let mut config = Config::new(None); - config.init()?; - - if let Action::Config = self.action { - tek_print_config(&config); - } else if let Action::List = self.action { - todo!("list sessions") - } else if let Action::Resume = self.action { - todo!("resume session") - } else if let Action::New { - name, bpm, tracks, scenes, sync_lead, sync_follow, - midi_from, midi_from_re, midi_to, midi_to_re, - left_from, right_from, left_to, right_to, .. - } = &self.action { - - // Connect to JACK - let name = name.as_ref().map_or("tek", |x|x.as_str()); - let jack = Jack::new(&name)?; - - // TODO: Collect audio IO: - let empty = &[] as &[&str]; - let left_froms = Connect::collect(&left_from, empty, empty); - let left_tos = Connect::collect(&left_to, empty, empty); - let right_froms = Connect::collect(&right_from, empty, empty); - let right_tos = Connect::collect(&right_to, empty, empty); - let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; - let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; - - // Create initial project: - let clock = Clock::new(&jack, *bpm)?; - let mut project = Arrangement::new( - &jack, - None, - clock, - vec![], - vec![], - Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() - .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) - .collect::, _>>()?, - Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() - .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) - .collect::, _>>()? - ); - project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; - project.scenes_add(scenes.unwrap_or(0))?; - - if matches!(self.action, Action::Status) { - // Show status and exit - tek_print_status(&project); - return Ok(()) - } - - // Initialize the app state - let app = tek(&jack, project, config, ":menu"); - if matches!(self.action, Action::Headless) { - // TODO: Headless mode (daemon + client over IPC, then over network...) - println!("todo headless"); - return Ok(()) - } - - // Run the [Tui] and [Jack] threads with the [App] state. - Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ - - // Between jack init and app's first cycle: - - jack.sync_lead(*sync_lead, |mut state|{ - let clock = app.clock(); - clock.playhead.update_from_sample(state.position.frame() as f64); - state.position.bbt = Some(clock.bbt()); - state.position - })?; - - jack.sync_follow(*sync_follow)?; - - // FIXME: They don't work properly. - - Ok(app) - - })?)?; - } - Ok(()) - } - } - - impl Config { - const CONFIG_DIR: &'static str = "tek"; - const CONFIG_SUB: &'static str = "v0"; - const CONFIG: &'static str = "tek.edn"; - const DEFAULTS: &'static str = include_str!("./tek.edn"); - /// Create a new app configuration from a set of XDG base directories, - pub fn new (dirs: Option) -> Self { - let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); - let dirs = dirs.unwrap_or_else(default); - Self { dirs, ..Default::default() } - } - /// Write initial contents of configuration. - pub fn init (&mut self) -> Usually<()> { - self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ - cfgs.add(&dsl)?; - Ok(()) - })?; - Ok(()) - } - /// Write initial contents of a configuration file. - pub fn init_one ( - &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> - ) -> Usually<()> { - if self.dirs.find_config_file(path).is_none() { - //println!("Creating {path:?}"); - std::fs::write(self.dirs.place_config_file(path)?, defaults)?; - } - Ok(if let Some(path) = self.dirs.find_config_file(path) { - //println!("Loading {path:?}"); - let src = std::fs::read_to_string(&path)?; - src.as_str().each(move|item|each(self, item))?; - } else { - return Err(format!("{path}: not found").into()) - }) - } - /// Add statements to configuration from [Dsl] source. - pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { - dsl.each(|item|self.add_one(item))?; - Ok(self) - } - fn add_one (&self, item: impl Language) -> Usually<()> { - if let Some(expr) = item.expr()? { - let head = expr.head()?; - let tail = expr.tail()?; - let name = tail.head()?; - let body = tail.tail()?; - //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); - match head { - Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, - Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, - Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, - _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) - } - Ok(()) - } else { - return Err(format!("Config::load: expected expr, got: {item:?}").into()) - } - } - pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { - self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() - } - } - - impl Mode> { - /// Add a definition to the mode. - /// - /// Supported definitions: - /// - /// - (name ...) -> name - /// - (info ...) -> description - /// - (keys ...) -> key bindings - /// - (mode ...) -> submode - /// - ... -> view - /// - /// ``` - /// let mut mode: tek::Mode> = Default::default(); - /// mode.add("(name hello)").unwrap(); - /// ``` - pub fn add (&mut self, dsl: impl Language) -> Usually<()> { - Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { - //println!("Mode::add: {head} {:?}", expr.tail()); - let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - match head { - "name" => self.add_name(tail)?, - "info" => self.add_info(tail)?, - "keys" => self.add_keys(tail)?, - "mode" => self.add_mode(tail)?, - _ => self.add_view(tail)?, - }; - } else if let Ok(Some(word)) = dsl.word() { - self.add_view(word); - } else { - return Err(format!("Mode::add: unexpected: {dsl:?}").into()); - }) - - //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) - //.word(|word|self.add_view(word)) - //.expr(|expr|expr.head(|head|{ - ////println!("Mode::add: {head} {:?}", expr.tail()); - //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - //match head { - //"name" => self.add_name(tail), - //"info" => self.add_info(tail), - //"keys" => self.add_keys(tail)?, - //"mode" => self.add_mode(tail)?, - //_ => self.add_view(tail), - //}; - //})) - } - - fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.name.push(src.into()))) - } - fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.info.push(src.into()))) - } - fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.view.push(src.into()))) - } - fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) - } - fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(if let Some(id) = dsl.head()? { - load_mode(&self.modes, &id, &dsl.tail())?; - } else { - return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); - })) - } - } - - impl Bind { - /// Create a new event map - pub fn new () -> Self { - Default::default() - } - /// Add a binding to an owned event map. - pub fn def (mut self, event: E, binding: Binding) -> Self { - self.add(event, binding); - self - } - /// Add a binding to an event map. - pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { - if !self.0.contains_key(&event) { - self.0.insert(event.clone(), Default::default()); - } - self.0.get_mut(&event).unwrap().push(binding); - self - } - /// Return the binding(s) that correspond to an event. - pub fn query (&self, event: &E) -> Option<&[Binding]> { - self.0.get(event).map(|x|x.as_slice()) - } - /// Return the first binding that corresponds to an event, considering conditions. - pub fn dispatch (&self, event: &E) -> Option<&Binding> { - self.query(event) - .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) - .flatten() - } - } - - impl Bind> { - pub fn load (lang: &impl Language) -> Usually { - let mut map = Bind::new(); - lang.each(|item|if item.expr().head() == Ok(Some("see")) { - // TODO - Ok(()) - } else if let Ok(Some(_word)) = item.expr().head().word() { - if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { - map.add(key, Binding { - commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), - condition: None, - description: None, - source: None - }); - Ok(()) - } else if Some(":char") == item.expr()?.head()? { - // TODO - return Ok(()) - } else { - return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) - } - } else { - return Err(format!("Config::load_bind: unexpected: {item:?}").into()) - })?; - Ok(map) - } - } } mod dialog { diff --git a/app/tek_struct.rs b/app/tek_struct.rs deleted file mode 100644 index 6af03ff8..00000000 --- a/app/tek_struct.rs +++ /dev/null @@ -1,1042 +0,0 @@ -use crate::*; -use clap::{self, Parser, Subcommand}; -use builder_pattern::Builder; - -/// Total state -/// -/// ``` -/// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; -/// let mut app = tek::App::default(); -/// let _ = app.scene_add(None, None).unwrap(); -/// let _ = app.update_clock(); -/// app.project.editor = Some(Default::default()); -/// //let _: Vec<_> = app.project.inputs_with_sizes().collect(); -/// //let _: Vec<_> = app.project.outputs_with_sizes().collect(); -/// let _: Vec<_> = app.project.tracks_with_sizes().collect(); -/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); -/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); -/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); -/// let _ = app.project.w(); -/// //let _ = app.project.w_sidebar(); -/// //let _ = app.project.w_tracks_area(); -/// let _ = app.project.h(); -/// //let _ = app.project.h_tracks_area(); -/// //let _ = app.project.h_inputs(); -/// //let _ = app.project.h_outputs(); -/// let _ = app.project.h_scenes(); -/// ``` -#[derive(Default, Debug)] pub struct App { - /// Base color. - pub color: ItemTheme, - /// Must not be dropped for the duration of the process - pub jack: Jack<'static>, - /// Display size - pub size: Measure, - /// Performance counter - pub perf: PerfModel, - /// Available view modes and input bindings - pub config: Config, - /// Currently selected mode - pub mode: Arc>>, - /// Undo history - pub history: Vec<(AppCommand, Option)>, - /// Dialog overlay - pub dialog: Dialog, - /// Contains all recently created clips. - pub pool: Pool, - /// Contains the currently edited musical arrangement - pub project: Arrangement, - /// Error, if any - pub error: Arc>>> -} - -/// Configuration: mode, view, and bind definitions. -/// -/// ``` -/// let config = tek::Config::default(); -/// ``` -/// -/// ``` -/// // Some dizzle. -/// // What indentation to use here lol? -/// let source = stringify!((mode :menu (name Menu) -/// (info Mode selector.) (keys :axis/y :confirm) -/// (view (bg (g 0) (bsp/s :ports/out -/// (bsp/n :ports/in -/// (bg (g 30) (bsp/s (fixed/y 7 :logo) -/// (fill :dialog/menu))))))))); -/// // Add this definition to the config and try to load it. -/// // A "mode" is basically a state machine -/// // with associated input and output definitions. -/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap(); -/// ``` -#[derive(Default, Debug)] pub struct Config { - /// XDG base directories of running user. - pub dirs: BaseDirectories, - /// Active collection of interaction modes. - pub modes: Modes, - /// Active collection of event bindings. - pub binds: Binds, - /// Active collection of view definitions. - pub views: Views, -} - -/// Group of view and keys definitions. -/// -/// ``` -/// let mode = tek::Mode::>::default(); -/// ``` -#[derive(Default, Debug)] pub struct Mode { - pub path: PathBuf, - pub name: Vec, - pub info: Vec, - pub view: Vec, - pub keys: Vec, - pub modes: Modes, -} - -/// An map of input events (e.g. [TuiEvent]) to [Binding]s. -/// -/// ``` -/// let lang = "(@x (nop)) (@y (nop) (nop))"; -/// let bind = tek::Bind::>::load(&lang).unwrap(); -/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1)); -/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2)); -/// ``` -#[derive(Debug)] pub struct Bind( - /// Map of each event (e.g. key combination) to - /// all command expressions bound to it by - /// all loaded input layers. - pub BTreeMap>> -); - -/// A sequence of zero or more commands (e.g. [AppCommand]), -/// optionally filtered by [Condition] to form layers. -/// -/// ``` -/// //FIXME: Why does it overflow? -/// //let binding: Binding<()> = tek::Binding { ..Default::default() }; -/// ``` -#[derive(Debug, Clone)] pub struct Binding { - pub commands: Arc<[C]>, - pub condition: Option, - pub description: Option>, - pub source: Option>, -} - -/// Condition that must evaluate to true in order to enable an input layer. -/// -/// ``` -/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true}))); -/// ``` -#[derive(Clone)] pub struct Condition( - pub Arcbool + Send + Sync>> -); - -/// List of menu items. -/// -/// ``` -/// let items: tek::MenuItems = Default::default(); -/// ``` -#[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems( - pub Arc<[MenuItem]> -); - -/// An item of a menu. -/// -/// ``` -/// let item: tek::MenuItem = Default::default(); -/// ``` -#[derive(Clone)] pub struct MenuItem( - /// Label - pub Arc, - /// Callback - pub ArcUsually<()> + Send + Sync>> -); - -/// The command-line interface descriptor. -/// -/// ``` -/// let cli: tek::Cli = Default::default(); -/// -/// use clap::CommandFactory; -/// tek::Cli::command().debug_assert(); -/// ``` -#[derive(Parser)] -#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] -#[derive(Debug, Default)] pub struct Cli { - /// Pre-defined configuration modes. - /// - /// TODO: Replace these with scripted configurations. - #[command(subcommand)] pub action: Action, -} - -/// Application modes that can be passed to the mommand line interface. -/// -/// ``` -/// let action: tek::Action = Default::default(); -/// ``` -#[derive(Debug, Clone, Subcommand, Default)] pub enum Action { - /// Continue where you left off - #[default] Resume, - /// Run headlessly in current session. - Headless, - /// Show status of current session. - Status, - /// List known sessions. - List, - /// Continue work in a copy of the current session. - Fork, - /// Create a new empty session. - New { - /// Name of JACK client - #[arg(short='n', long)] name: Option, - /// Whether to attempt to become transport master - #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, - /// Whether to sync to external transport master - #[arg(short='y', long, default_value_t = true)] sync_follow: bool, - /// Initial tempo in beats per minute - #[arg(short='b', long, default_value = None)] bpm: Option, - /// Whether to include a transport toolbar (default: true) - #[arg(short='c', long, default_value_t = true)] show_clock: bool, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='I', long)] midi_from: Vec, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='i', long)] midi_from_re: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='O', long)] midi_to: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='o', long)] midi_to_re: Vec, - /// Audio outs to connect to left input - #[arg(short='l', long)] left_from: Vec, - /// Audio outs to connect to right input - #[arg(short='r', long)] right_from: Vec, - /// Audio ins to connect from left output - #[arg(short='L', long)] left_to: Vec, - /// Audio ins to connect from right output - #[arg(short='R', long)] right_to: Vec, - /// Tracks to create - #[arg(short='t', long)] tracks: Option, - /// Scenes to create - #[arg(short='s', long)] scenes: Option, - }, - /// Import media as new session. - Import, - /// Show configuration. - Config, - /// Show version. - Version, -} - -/// A control axis. -/// -/// ``` -/// let axis = tek::ControlAxis::X; -/// ``` -#[derive(Debug, Copy, Clone)] pub enum ControlAxis { - X, Y, Z, I -} - -/// Various possible dialog modes. -/// -/// ``` -/// let dialog: tek::Dialog = Default::default(); -/// ``` -#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { - #[default] None, - Help(usize), - Menu(usize, MenuItems), - Device(usize), - Message(Arc), - Browse(BrowseTarget, Arc), - Options, -} - -/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) -/// -/// ``` -/// let _ = tek::Timebase::default(); -/// ``` -#[derive(Debug, Clone)] pub struct Timebase { - /// Audio samples per second - pub sr: SampleRate, - /// MIDI beats per minute - pub bpm: Bpm, - /// MIDI ticks per beat - pub ppq: Ppq, -} - -/// Iterator that emits subsequent ticks within a range. -/// -/// ``` -/// let iter = tek::Ticker::default(); -/// ``` -#[derive(Debug, Default)] pub struct Ticker { - pub spp: f64, - pub sample: usize, - pub start: usize, - pub end: usize, -} - -/// -/// ``` -/// let _ = tek::MidiCursor::default(); -/// ``` -#[derive(Debug, Clone)] pub struct MidiCursor { - /// Time coordinate of cursor - pub time_pos: Arc, - /// Note coordinate of cursor - pub note_pos: Arc, - /// Length of note that will be inserted, in pulses - pub note_len: Arc, -} - -/// -/// ``` -/// use tek::{TimeRange, NoteRange}; -/// let model = tek::MidiSelection::from((1, false)); -/// -/// let _ = model.get_time_len(); -/// let _ = model.get_time_zoom(); -/// let _ = model.get_time_lock(); -/// let _ = model.get_time_start(); -/// let _ = model.get_time_axis(); -/// let _ = model.get_time_end(); -/// -/// let _ = model.get_note_lo(); -/// let _ = model.get_note_axis(); -/// let _ = model.get_note_hi(); -/// ``` -#[derive(Debug, Clone, Default)] pub struct MidiSelection { - pub time_len: Arc, - /// Length of visible time axis - pub time_axis: Arc, - /// Earliest time displayed - pub time_start: Arc, - /// Time step - pub time_zoom: Arc, - /// Auto rezoom to fit in time axis - pub time_lock: Arc, - /// Length of visible note axis - pub note_axis: Arc, - // Lowest note displayed - pub note_lo: Arc, -} - -/// A point in time in all time scales (microsecond, sample, MIDI pulse) -/// -/// ``` -/// let _ = tek::Moment::default(); -/// ``` -#[derive(Debug, Default, Clone)] pub struct Moment { - pub timebase: Arc, - /// Current time in microseconds - pub usec: Microsecond, - /// Current time in audio samples - pub sample: SampleCount, - /// Current time in MIDI pulses - pub pulse: Pulse, -} - -/// -/// ``` -/// let _ = tek::Moment2::default(); -/// ``` -#[derive(Debug, Clone, Default)] pub enum Moment2 { - #[default] None, - Zero, - Usec(Microsecond), - Sample(SampleCount), - Pulse(Pulse), -} - -/// MIDI resolution in PPQ (pulses per quarter note) -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64); - -/// Timestamp in MIDI pulses -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64); - -/// Tempo in beats per minute -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64); - -/// Quantization setting for launching clips -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64); - -/// Quantization setting for notes -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64); - -/// Timestamp in audio samples -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64); - -/// Audio sample rate in Hz (samples per second) -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64); - -/// Timestamp in microseconds -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64); - -/// The source of time. -/// -/// ``` -/// let clock = tek::Clock::default(); -/// ``` -#[derive(Clone, Default)] pub struct Clock { - /// JACK transport handle. - pub transport: Arc>, - /// Global temporal resolution (shared by [Moment] fields) - pub timebase: Arc, - /// Current global sample and usec (monotonic from JACK clock) - pub global: Arc, - /// Global sample and usec at which playback started - pub started: Arc>>, - /// Playback offset (when playing not from start) - pub offset: Arc, - /// Current playhead position - pub playhead: Arc, - /// Note quantization factor - pub quant: Arc, - /// Launch quantization factor - pub sync: Arc, - /// Size of buffer in samples - pub chunk: Arc, - // Cache of formatted strings - pub view_cache: Arc>, - /// For syncing the clock to an external source - #[cfg(feature = "port")] pub midi_in: Arc>>, - /// For syncing other devices to this clock - #[cfg(feature = "port")] pub midi_out: Arc>>, - /// For emitting a metronome - #[cfg(feature = "port")] pub click_out: Arc>>, -} - -/// Contains memoized renders of clock values. -/// -/// Performance optimization. -#[derive(Debug)] pub struct ClockView { - pub sr: Memo, String>, - pub buf: Memo, String>, - pub lat: Memo, String>, - pub bpm: Memo, String>, - pub beat: Memo, String>, - pub time: Memo, String>, -} - -/// Arranger. -/// -/// ``` -/// let arranger = tek::Arrangement::default(); -/// ``` -#[derive(Default, Debug)] pub struct Arrangement { - /// Project name. - pub name: Arc, - /// Base color. - pub color: ItemTheme, - /// JACK client handle. - pub jack: Jack<'static>, - /// FIXME a render of the project arrangement, redrawn on update. - /// TODO rename to "render_cache" or smth - pub arranger: Arc>, - /// Display size - pub size: Measure, - /// Display size of clips area - pub size_inner: Measure, - /// Source of time - #[cfg(feature = "clock")] pub clock: Clock, - /// Allows one MIDI clip to be edited - #[cfg(feature = "editor")] pub editor: Option, - /// List of global midi inputs - #[cfg(feature = "port")] pub midi_ins: Vec, - /// List of global midi outputs - #[cfg(feature = "port")] pub midi_outs: Vec, - /// List of global audio inputs - #[cfg(feature = "port")] pub audio_ins: Vec, - /// List of global audio outputs - #[cfg(feature = "port")] pub audio_outs: Vec, - /// Selected UI element - #[cfg(feature = "select")] pub selection: Selection, - /// Last track number (to avoid duplicate port names) - #[cfg(feature = "track")] pub track_last: usize, - /// List of tracks - #[cfg(feature = "track")] pub tracks: Vec, - /// Scroll offset of tracks - #[cfg(feature = "track")] pub track_scroll: usize, - /// List of scenes - #[cfg(feature = "scene")] pub scenes: Vec, - /// Scroll offset of scenes - #[cfg(feature = "scene")] pub scene_scroll: usize, -} - -/// Browses for files to load/save. -/// -/// ``` -/// let browse = tek::Browse::default(); -/// ``` -#[derive(Debug, Clone, Default, PartialEq)] pub struct Browse { - pub cwd: PathBuf, - pub dirs: Vec<(OsString, String)>, - pub files: Vec<(OsString, String)>, - pub filter: String, - pub index: usize, - pub scroll: usize, - pub size: Measure, -} - -pub(crate) struct EntriesIterator<'a> { - pub browser: &'a Browse, - pub offset: usize, - pub length: usize, - pub index: usize, -} - -#[derive(Clone, Debug)] pub enum BrowseTarget { - SaveProject, - LoadProject, - ImportSample(Arc>>), - ExportSample(Arc>>), - ImportClip(Arc>>), - ExportClip(Arc>>), -} - -/// A MIDI sequence. -/// -/// ``` -/// let clip = tek::MidiClip::default(); -/// ``` -#[derive(Debug, Clone, Default)] pub struct MidiClip { - pub uuid: uuid::Uuid, - /// Name of clip - pub name: Arc, - /// Temporal resolution in pulses per quarter note - pub ppq: usize, - /// Length of clip in pulses - pub length: usize, - /// Notes in clip - pub notes: MidiData, - /// Whether to loop the clip or play it once - pub looped: bool, - /// Start of loop - pub loop_start: usize, - /// Length of loop - pub loop_length: usize, - /// All notes are displayed with minimum length - pub percussive: bool, - /// Identifying color of clip - pub color: ItemTheme, -} - -/// A device that can be plugged into the chain. -/// -/// ``` -/// let device = tek::Device::default(); -/// ``` -#[derive(Debug, Default)] pub enum Device { - #[default] - Bypass, - Mute, - #[cfg(feature = "sampler")] - Sampler(Sampler), - #[cfg(feature = "lv2")] // TODO - Lv2(Lv2), - #[cfg(feature = "vst2")] // TODO - Vst2, - #[cfg(feature = "vst3")] // TODO - Vst3, - #[cfg(feature = "clap")] // TODO - Clap, - #[cfg(feature = "sf2")] // TODO - Sf2, -} - -/// Some sort of wrapper? -pub struct DeviceAudio<'a>(pub &'a mut Device); - -/// Contains state for viewing and editing a clip. -/// -/// ``` -/// use std::sync::{Arc, RwLock}; -/// let clip = tek::MidiClip::stop_all(); -/// let mut editor = tek::MidiEditor { -/// mode: tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip)))), -/// size: Default::default(), -/// //keys: Default::default(), -/// }; -/// let _ = editor.put_note(true); -/// let _ = editor.put_note(false); -/// let _ = editor.clip_status(); -/// let _ = editor.edit_status(); -/// ``` -pub struct MidiEditor { - /// Size of editor on screen - pub size: Measure, - /// View mode and state of editor - pub mode: PianoHorizontal, -} - -/// A clip, rendered as a horizontal piano roll. -/// -/// ``` -/// let piano = tek::PianoHorizontal::default(); -/// ``` -#[derive(Clone, Default)] pub struct PianoHorizontal { - pub clip: Option>>, - /// Buffer where the whole clip is rerendered on change - pub buffer: Arc>, - /// Size of actual notes area - pub size: Measure, - /// The display window - pub range: MidiSelection, - /// The note cursor - pub point: MidiCursor, - /// The highlight color palette - pub color: ItemTheme, - /// Width of the keyboard - pub keys_width: u16, -} - -/// 12 piano keys, some highlighted. -/// -/// ``` -/// let keys = tek::OctaveVertical::default(); -/// ``` -#[derive(Copy, Clone)] pub struct OctaveVertical { - pub on: [bool; 12], - pub colors: [Color; 3] -} - -/// A LV2 plugin. -#[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 { - /// JACK client handle (needs to not be dropped for standalone mode to work). - pub jack: Jack<'static>, - pub name: Arc, - pub path: Option>, - pub selected: usize, - pub mapping: bool, - pub midi_ins: Vec>, - pub midi_outs: Vec>, - pub audio_ins: Vec>, - pub audio_outs: Vec>, - - pub lv2_world: livi::World, - pub lv2_instance: livi::Instance, - pub lv2_plugin: livi::Plugin, - pub lv2_features: Arc, - pub lv2_port_list: Vec, - pub lv2_input_buffer: Vec, - pub lv2_ui_thread: Option>, -} - -/// A LV2 plugin's X11 UI. -#[cfg(feature = "lv2_gui")] pub struct LV2PluginUI { - pub window: Option -} - -#[derive(Debug, Default)] pub enum MeteringMode { - #[default] Rms, - Log10, -} - -#[derive(Debug, Default, Clone)] pub struct Log10Meter(pub f32); - -#[derive(Debug, Default, Clone)] pub struct RmsMeter(pub f32); - -#[derive(Debug, Default)] pub enum MixingMode { - #[default] Summing, - Average, -} - -/// A clip pool. -/// -/// ``` -/// let pool = tek::Pool::default(); -/// ``` -#[derive(Debug)] pub struct Pool { - pub visible: bool, - /// Selected clip - pub clip: AtomicUsize, - /// Mode switch - pub mode: Option, - /// Embedded file browse - #[cfg(feature = "browse")] pub browse: Option, - /// Collection of MIDI clips. - #[cfg(feature = "clip")] pub clips: Arc>>>>, - /// Collection of sound samples. - #[cfg(feature = "sampler")] pub samples: Arc>>>>, -} - -/// Displays and edits clip length. -#[derive(Clone, Debug, Default)] pub struct ClipLength { - /// Pulses per beat (quaver) - pub ppq: usize, - /// Beats per bar - pub bpb: usize, - /// Length of clip in pulses - pub pulses: usize, - /// Selected subdivision - pub focus: Option, -} - -/// Some sort of wrapper again? -pub struct PoolView<'a>(pub &'a Pool); - -/// Audio input port. -#[derive(Debug)] pub struct AudioInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// Audio output port. -#[derive(Debug)] pub struct AudioOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI input port. -#[derive(Debug)] pub struct MidiInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of currently held notes. - pub held: Arc>, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI output port. -#[derive(Debug)] pub struct MidiOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, - /// List of currently held notes. - pub held: Arc>, - /// Buffer - pub note_buffer: Vec, - /// Buffer - pub output_buffer: Vec>>, -} - -/// Port connection manager. -/// -/// ``` -/// let connect = tek::Connect::default(); -/// ``` -#[derive(Clone, Debug, Default)] pub struct Connect { - pub name: Option, - pub scope: Option, - pub status: Arc, Arc, ConnectStatus)>>>, - pub info: Arc, -} - -/// Plays [Voice]s from [Sample]s. -/// -/// ``` -/// let sampler = tek::Sampler::default(); -/// ``` -#[derive(Debug, Default)] pub struct Sampler { - /// Name of sampler. - pub name: Arc, - /// Device color. - pub color: ItemTheme, - /// Sample currently being recorded. - pub recording: Option<(usize, Option>>)>, - /// Recording buffer. - pub buffer: Vec>, - /// Samples mapped to MIDI notes. - pub samples: SampleKit<128>, - /// Collection of currently playing instances of samples. - pub voices: Arc>>, - /// Samples that are not mapped to MIDI notes. - pub unmapped: Vec>>, - /// Sample currently being edited. - pub editing: Option>>, - /// How to mix the voices. - pub mixing_mode: MixingMode, - /// How to meter the inputs and outputs. - pub metering_mode: MeteringMode, - /// Fixed gain applied to all output. - pub output_gain: f32, - /// Currently active modal, if any. - pub mode: Option, - /// Size of rendered sampler. - pub size: Measure, - /// Lowest note displayed. - pub note_lo: AtomicUsize, - /// Currently selected note. - pub note_pt: AtomicUsize, - /// Selected note as row/col. - pub cursor: (AtomicUsize, AtomicUsize), - /// Audio input meters. - #[cfg(feature = "meter")] pub input_meters: Vec, - /// Audio input ports. Samples are recorded from here. - #[cfg(feature = "port")] pub audio_ins: Vec, - /// MIDI input port. Sampler are triggered from here. - #[cfg(feature = "port")] pub midi_in: Option, - /// Audio output ports. Voices are played into here. - #[cfg(feature = "port")] pub audio_outs: Vec, - /// Audio output meters. - #[cfg(feature = "meter")] pub output_meters: Vec, -} - -/// Collection of samples, one per slot, fixed number of slots. -/// -/// History: Separated to cleanly implement [Default]. -/// -/// ``` -/// let samples = tek::SampleKit([None, None, None, None]); -/// ``` -#[derive(Debug)] pub struct SampleKit( - pub [Option>>;N] -); - -/// A sound cut. -/// -/// ``` -/// let sample = tek::Sample::default(); -/// let sample = tek::Sample::new("test", 0, 0, vec![]); -/// ``` -#[derive(Default, Debug)] pub struct Sample { - pub name: Arc, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, - pub gain: f32, - pub color: ItemTheme, -} - -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - -#[derive(Default, Debug)] pub struct SampleAdd { - pub exited: bool, - pub dir: PathBuf, - pub subdirs: Vec, - pub files: Vec, - pub cursor: usize, - pub offset: usize, - pub sample: Arc>, - pub voices: Arc>>, - pub _search: Option, -} - -/// A scene consists of a set of clips to play together. -/// -/// ``` -/// let scene: tek::Scene = Default::default(); -/// let _ = scene.pulses(); -/// let _ = scene.is_playing(&[]); -/// ``` -#[derive(Debug, Default)] pub struct Scene { - /// Name of scene - pub name: Arc, - /// Identifying color of scene - pub color: ItemTheme, - /// Clips in scene, one per track - pub clips: Vec>>>, -} - -/// Represents the current user selection in the arranger -#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { - #[default] - /// Nothing is selected - Nothing, - /// The whole mix is selected - Mix, - /// A MIDI input is selected. - Input(usize), - /// A MIDI output is selected. - Output(usize), - /// A scene is selected. - #[cfg(feature = "scene")] Scene(usize), - /// A track is selected. - #[cfg(feature = "track")] Track(usize), - /// A clip (track × scene) is selected. - #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, - /// A track's MIDI input connection is selected. - #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, - /// A track's MIDI output connection is selected. - #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, - /// A track device slot is selected. - #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, -} - -/// Contains state for playing a clip -/// -/// ``` -/// let clip = tek::MidiClip::default(); -/// println!("Empty clip: {clip:?}"); -/// -/// let clip = tek::MidiClip::stop_all(); -/// println!("Panic clip: {clip:?}"); -/// -/// let mut clip = tek::MidiClip::new("clip", true, 1, None, None); -/// clip.set_length(96); -/// clip.toggle_loop(); -/// clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); -/// assert!(clip.contains_note_on(36.into(), 6, 18)); -/// assert_eq!(&clip.notes, &clip.duplicate().notes); -/// -/// let clip = std::sync::Arc::new(clip); -/// assert_eq!(clip.clone(), clip); -/// -/// let sequencer = tek::Sequencer::default(); -/// println!("{sequencer:?}"); -/// ``` -pub struct Sequencer { - /// State of clock and playhead - #[cfg(feature = "clock")] pub clock: Clock, - /// Start time and clip being played - #[cfg(feature = "clip")] pub play_clip: Option<(Moment, Option>>)>, - /// Start time and next clip - #[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option>>)>, - /// Record from MIDI ports to current sequence. - #[cfg(feature = "port")] pub midi_ins: Vec, - /// Play from current sequence to MIDI ports - #[cfg(feature = "port")] pub midi_outs: Vec, - /// Play input through output. - pub monitoring: bool, - /// Write input to sequence. - pub recording: bool, - /// Overdub input to sequence. - pub overdub: bool, - /// Send all notes off - pub reset: bool, // TODO?: after Some(nframes) - /// Notes currently held at input - pub notes_in: Arc>, - /// Notes currently held at output - pub notes_out: Arc>, - /// MIDI output buffer - pub note_buf: Vec, - /// MIDI output buffer - pub midi_buf: Vec>>, -} - -/// A track consists of a sequencer and zero or more devices chained after it. -/// -/// ``` -/// let track: tek::Track = Default::default(); -/// ``` -#[derive(Debug, Default)] pub struct Track { - /// Name of track - pub name: Arc, - /// Identifying color of track - pub color: ItemTheme, - /// Preferred width of track column - pub width: usize, - /// MIDI sequencer state - pub sequencer: Sequencer, - /// Device chain - pub devices: Vec, -} - -// Commands supported by [Browse] -//#[derive(Debug, Clone, PartialEq)] -//pub enum BrowseCommand { - //Begin, - //Cancel, - //Confirm, - //Select(usize), - //Chdir(PathBuf), - //Filter(Arc), -//} - -/// Modes for clip pool -#[derive(Debug, Clone)] pub enum PoolMode { - /// Renaming a pattern - Rename(usize, Arc), - /// Editing the length of a pattern - Length(usize, usize, ClipLengthFocus), - /// Load clip from disk - Import(usize, Browse), - /// Save clip to disk - Export(usize, Browse), -} - -/// Focused field of `ClipLength` -#[derive(Copy, Clone, Debug)] pub enum ClipLengthFocus { - /// Editing the number of bars - Bar, - /// Editing the number of beats - Beat, - /// Editing the number of ticks - Tick, -} - -#[derive(Clone, Debug, PartialEq)] pub enum ConnectName { - /** Exact match */ - Exact(Arc), - /** Match regular expression */ - RegExp(Arc), -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { - One, - All -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { - Missing, - Disconnected, - Connected, - Mismatch, -} - -#[derive(Debug)] pub enum SamplerMode { - // Load sample from path - Import(usize, Browse), -} diff --git a/app/tek_trait.rs b/app/tek_trait.rs deleted file mode 100644 index 57fdde33..00000000 --- a/app/tek_trait.rs +++ /dev/null @@ -1,919 +0,0 @@ -use crate::*; -use std::sync::atomic::Ordering; - -pub trait Gettable { - /// Returns current value - fn get (&self) -> T; -} - -pub trait Mutable: Gettable { - /// Sets new value, returns old - fn set (&mut self, value: T) -> T; -} - -pub trait InteriorMutable: Gettable { - /// Sets new value, returns old - fn set (&self, value: T) -> T; -} - -pub trait NotePoint { - fn note_len (&self) -> &AtomicUsize; - /// Get the current length of the note cursor. - fn get_note_len (&self) -> usize { - self.note_len().load(Relaxed) - } - /// Set the length of the note cursor, returning the previous value. - fn set_note_len (&self, x: usize) -> usize { - self.note_len().swap(x, Relaxed) - } - - fn note_pos (&self) -> &AtomicUsize; - /// Get the current pitch of the note cursor. - fn get_note_pos (&self) -> usize { - self.note_pos().load(Relaxed).min(127) - } - /// Set the current pitch fo the note cursor, returning the previous value. - fn set_note_pos (&self, x: usize) -> usize { - self.note_pos().swap(x.min(127), Relaxed) - } -} - -pub trait TimePoint { - fn time_pos (&self) -> &AtomicUsize; - /// Get the current time position of the note cursor. - fn get_time_pos (&self) -> usize { - self.time_pos().load(Relaxed) - } - /// Set the current time position of the note cursor, returning the previous value. - fn set_time_pos (&self, x: usize) -> usize { - self.time_pos().swap(x, Relaxed) - } -} - -pub trait MidiPoint: NotePoint + TimePoint { - /// Get the current end of the note cursor. - fn get_note_end (&self) -> usize { - self.get_time_pos() + self.get_note_len() - } -} - -pub trait TimeRange { - fn time_len (&self) -> &AtomicUsize; - fn get_time_len (&self) -> usize { - self.time_len().load(Ordering::Relaxed) - } - fn time_zoom (&self) -> &AtomicUsize; - fn get_time_zoom (&self) -> usize { - self.time_zoom().load(Ordering::Relaxed) - } - fn set_time_zoom (&self, value: usize) -> usize { - self.time_zoom().swap(value, Ordering::Relaxed) - } - fn time_lock (&self) -> &AtomicBool; - fn get_time_lock (&self) -> bool { - self.time_lock().load(Ordering::Relaxed) - } - fn set_time_lock (&self, value: bool) -> bool { - self.time_lock().swap(value, Ordering::Relaxed) - } - fn time_start (&self) -> &AtomicUsize; - fn get_time_start (&self) -> usize { - self.time_start().load(Ordering::Relaxed) - } - fn set_time_start (&self, value: usize) -> usize { - self.time_start().swap(value, Ordering::Relaxed) - } - fn time_axis (&self) -> &AtomicUsize; - fn get_time_axis (&self) -> usize { - self.time_axis().load(Ordering::Relaxed) - } - fn get_time_end (&self) -> usize { - self.time_start().get() + self.time_axis().get() * self.time_zoom().get() - } -} - -pub trait NoteRange { - fn note_lo (&self) -> &AtomicUsize; - fn get_note_lo (&self) -> usize { - self.note_lo().load(Ordering::Relaxed) - } - fn set_note_lo (&self, x: usize) -> usize { - self.note_lo().swap(x, Ordering::Relaxed) - } - fn note_axis (&self) -> &AtomicUsize; - fn get_note_axis (&self) -> usize { - self.note_axis().load(Ordering::Relaxed) - } - fn get_note_hi (&self) -> usize { - (self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127) - } -} - -pub trait MidiRange: TimeRange + NoteRange {} - -/// A unit of time, represented as an atomic 64-bit float. -/// -/// According to https://stackoverflow.com/a/873367, as per IEEE754, -/// every integer between 1 and 2^53 can be represented exactly. -/// This should mean that, even at 192kHz sampling rate, over 1 year of audio -/// can be clocked in microseconds with f64 without losing precision. -pub trait TimeUnit: InteriorMutable {} - -pub trait HasClipsSize { fn clips_size (&self) -> &Measure; } - -pub trait HasMidiClip { - fn clip (&self) -> Option>>; -} -pub trait HasClock: AsRef + AsMut { - fn clock (&self) -> &Clock { self.as_ref() } - fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } -} -pub trait HasDevices: AsRef> + AsMut> { - fn devices (&self) -> &Vec { self.as_ref() } - fn devices_mut (&mut self) -> &mut Vec { self.as_mut() } -} -pub trait HasSequencer: AsRef + AsMut { - fn sequencer_mut (&mut self) -> &mut Sequencer { self.as_mut() } - fn sequencer (&self) -> &Sequencer { self.as_ref() } -} -pub trait HasSceneScroll: HasScenes { fn scene_scroll (&self) -> usize; } -pub trait HasTrackScroll: HasTracks { fn track_scroll (&self) -> usize; } -pub trait HasScene: AsRefOpt + AsMutOpt { - fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() } - fn scene (&self) -> Option<&Scene> { self.as_ref_opt() } -} -pub trait HasSelection: AsRef + AsMut { - fn selection (&self) -> &Selection { self.as_ref() } - fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } - /// Get the active track - #[cfg(feature = "track")] - fn selected_track (&self) -> Option<&Track> where Self: HasTracks { - let index = self.selection().track()?; - self.tracks().get(index) - } - /// Get a mutable reference to the active track - #[cfg(feature = "track")] - fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { - let index = self.selection().track()?; - self.tracks_mut().get_mut(index) - } - /// Get the active scene - #[cfg(feature = "scene")] - fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { - let index = self.selection().scene()?; - self.scenes().get(index) - } - /// Get a mutable reference to the active scene - #[cfg(feature = "scene")] - fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { - let index = self.selection().scene()?; - self.scenes_mut().get_mut(index) - } - /// Get the active clip - #[cfg(feature = "clip")] - fn selected_clip (&self) -> Option>> where Self: HasScenes + HasTracks { - self.selected_scene()?.clips.get(self.selection().track()?)?.clone() - } -} -pub trait HasScenes: AsRef> + AsMut> { - fn scenes (&self) -> &Vec { self.as_ref() } - fn scenes_mut (&mut self) -> &mut Vec { self.as_mut() } - /// Generate the default name for a new scene - fn scene_default_name (&self) -> Arc { format!("s{:3>}", self.scenes().len() + 1).into() } - fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) } - /// Add multiple scenes - fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks { - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..n { - let _ = self.scene_add(None, Some( - scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() - ))?; - } - Ok(()) - } - /// Add a scene - fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<(usize, &mut Scene)> where Self: HasTracks - { - let scene = Scene { - 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(ItemTheme::random), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok((index, &mut self.scenes_mut()[index])) - } -} -pub trait HasWidth { - const MIN_WIDTH: usize; - /// Increment track width. - fn width_inc (&mut self); - /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. - fn width_dec (&mut self); -} -pub trait HasMidiBuffers { - fn note_buf_mut (&mut self) -> &mut Vec; - fn midi_buf_mut (&mut self) -> &mut Vec>>; -} - -/// ``` -/// use tek::{*, tengri::*}; -/// -/// struct Test(Option); -/// impl_as_ref_opt!(MidiEditor: |self: Test|self.0.as_ref()); -/// impl_as_mut_opt!(MidiEditor: |self: Test|self.0.as_mut()); -/// -/// let mut host = Test(Some(MidiEditor::default())); -/// let _ = host.editor(); -/// let _ = host.editor_mut(); -/// let _ = host.is_editing(); -/// let _ = host.editor_w(); -/// let _ = host.editor_h(); -/// ``` -pub trait HasEditor: AsRefOpt + AsMutOpt { - fn editor (&self) -> Option<&MidiEditor> { self.as_ref_opt() } - fn editor_mut (&mut self) -> Option<&mut MidiEditor> { self.as_mut_opt() } - fn is_editing (&self) -> bool { self.editor().is_some() } - fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) as usize } - fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) as usize } -} -pub trait HasClips { - fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; - fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>; - fn add_clip (&self) -> (usize, Arc>) { - let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None))); - self.clips_mut().push(clip.clone()); - (self.clips().len() - 1, clip) - } -} -/// Trait for thing that may receive MIDI. -pub trait HasMidiIns { - fn midi_ins (&self) -> &Vec; - fn midi_ins_mut (&mut self) -> &mut Vec; - /// Collect MIDI input from app ports (TODO preallocate large buffers) - fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { - self.midi_ins().iter() - .map(|port|port.port().iter(scope) - .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) - .collect::>()) - .collect::>() - } - fn midi_ins_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_ins().iter().enumerate().map(move|(i, input)|{ - let height = 1 + input.connections().len(); - let data = (i, input.port_name(), input.connections(), y, y + height); - y += height; - data - }) - } -} -/// Trait for thing that may output MIDI. -pub trait HasMidiOuts { - fn midi_outs (&self) -> &Vec; - fn midi_outs_mut (&mut self) -> &mut Vec; - fn midi_outs_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_outs().iter().enumerate().map(move|(i, output)|{ - let height = 1 + output.connections().len(); - let data = (i, output.port_name(), output.connections(), y, y + height); - y += height; - data - }) - } - fn midi_outs_emit (&mut self, scope: &ProcessScope) { - for port in self.midi_outs_mut().iter_mut() { - port.buffer_emit(scope) - } - } -} -pub trait HasTracks: AsRef> + AsMut> { - fn tracks (&self) -> &Vec { self.as_ref() } - fn tracks_mut (&mut self) -> &mut Vec { self.as_mut() } - /// Run audio callbacks for every track and every device - fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { - for track in self.tracks_mut().iter_mut() { - if Control::Quit == Audio::process(&mut track.sequencer, client, scope) { - return Control::Quit - } - for device in track.devices.iter_mut() { - if Control::Quit == DeviceAudio(device).process(client, scope) { - return Control::Quit - } - } - } - Control::Continue - } - fn track_longest_name (&self) -> usize { self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) } - /// Stop all playing clips - fn tracks_stop_all (&mut self) { for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } } - /// Stop all playing clips - fn tracks_launch (&mut self, clips: Option>>>>) { - if let Some(clips) = clips { - for (clip, track) in clips.iter().zip(self.tracks_mut()) { track.sequencer.enqueue_next(clip.as_ref()); } - } else { - for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } - } - } - /// Spacing between tracks. - const TRACK_SPACING: usize = 0; -} -pub trait HasTrack: AsRefOpt + AsMutOpt { - fn track (&self) -> Option<&Track> { self.as_ref_opt() } - fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() } - #[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw + 'a { - self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) - } - #[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw + '_ { - self.track().map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) - } - #[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw { - self.track().map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) - } - #[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw { - self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) - } -} - -pub trait HasPlayClip: HasClock { - - fn reset (&self) -> bool; - - fn reset_mut (&mut self) -> &mut bool; - - fn play_clip (&self) -> &Option<(Moment, Option>>)>; - - fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; - - fn next_clip (&self) -> &Option<(Moment, Option>>)>; - - fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; - - fn pulses_since_start (&self) -> Option { - if let Some((started, Some(_))) = self.play_clip().as_ref() { - let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); - return Some(elapsed) - } - None - } - - fn pulses_since_start_looped (&self) -> Option<(f64, f64)> { - if let Some((started, Some(clip))) = self.play_clip().as_ref() { - let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); - let length = clip.read().unwrap().length.max(1); // prevent div0 on empty clip - let times = (elapsed as usize / length) as f64; - let elapsed = (elapsed as usize % length) as f64; - return Some((times, elapsed)) - } - None - } - - fn enqueue_next (&mut self, clip: Option<&Arc>>) { - *self.next_clip_mut() = Some((self.clock().next_launch_instant(), clip.cloned())); - *self.reset_mut() = true; - } - - fn play_status (&self) -> impl Draw { - let (name, color): (Arc, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() { - let MidiClip { ref name, color, .. } = *clip.read().unwrap(); - (name.clone(), color) - } else { - ("".into(), Tui::g(64).into()) - }; - let time: String = self.pulses_since_start_looped() - .map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time))) - .unwrap_or_else(||String::from(" ")).into(); - field_v(color, "Now:", format!("{} {}", time, name)) - } - - fn next_status (&self) -> impl Draw { - let mut time: Arc = String::from("--.-.--").into(); - let mut name: Arc = String::from("").into(); - let mut color = ItemTheme::G[64]; - let clock = self.clock(); - if let Some((t, Some(clip))) = self.next_clip() { - let clip = clip.read().unwrap(); - name = clip.name.clone(); - color = clip.color.clone(); - time = { - let target = t.pulse.get(); - let current = clock.playhead.pulse.get(); - if target > current { - let remaining = target - current; - format!("-{:>}", clock.timebase.format_beats_1(remaining)) - } else { - String::new() - } - }.into() - } else if let Some((t, Some(clip))) = self.play_clip() { - let clip = clip.read().unwrap(); - if clip.looped { - name = clip.name.clone(); - color = clip.color.clone(); - let target = t.pulse.get() + clip.length as f64; - let current = clock.playhead.pulse.get(); - if target > current { - time = format!("-{:>}", clock.timebase.format_beats_0(target - current)).into() - } - } else { - name = "Stop".to_string().into(); - } - }; - field_v(color, "Next:", format!("{} {}", time, name)) - } -} - -pub trait MidiMonitor: HasMidiIns + HasMidiBuffers { - /// Input note flags. - fn notes_in (&self) -> &Arc>; - /// Current monitoring status. - fn monitoring (&self) -> bool; - /// Mutable monitoring status. - fn monitoring_mut (&mut self) -> &mut bool; - /// Enable or disable monitoring. - fn toggle_monitor (&mut self) { *self.monitoring_mut() = !self.monitoring(); } - /// Perform monitoring. - fn monitor (&mut self, _scope: &ProcessScope) { /* do nothing by default */ } -} - -pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { - fn recording (&self) -> bool; - fn recording_mut (&mut self) -> &mut bool; - fn toggle_record (&mut self) { - *self.recording_mut() = !self.recording(); - } - - fn overdub (&self) -> bool; - fn overdub_mut (&mut self) -> &mut bool; - fn toggle_overdub (&mut self) { - *self.overdub_mut() = !self.overdub(); - } - - fn record_clip ( - &mut self, - scope: &ProcessScope, - started: Moment, - clip: &Option>>, - ) { - if let Some(clip) = clip { - let sample0 = scope.last_frame_time() as usize; - let start = started.sample.get() as usize; - let _recording = self.recording(); - let timebase = self.clock().timebase().clone(); - let quant = self.clock().quant.get(); - let mut clip = clip.write().unwrap(); - let length = clip.length; - for input in self.midi_ins_mut().iter() { - for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) { - if let LiveEvent::Midi { message, .. } = event { - clip.record_event({ - let sample = (sample0 + sample - start) as f64; - let pulse = timebase.samples_to_pulse(sample); - let quantized = (pulse / quant).round() * quant; - quantized as usize % length - }, message); - } - } - } - } - } - - fn record_next (&mut self) { - // TODO switch to next clip and record into it - } -} - -pub trait MidiViewer: Measured + MidiRange + MidiPoint + Debug + Send + Sync { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize); - fn redraw (&self); - fn clip (&self) -> &Option>>; - fn clip_mut (&mut self) -> &mut Option>>; - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.redraw(); - } - /// Make sure cursor is within note range - fn autoscroll (&self) { - let note_pos = self.get_note_pos().min(127); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - if note_pos < note_lo { - self.note_lo().set(note_pos); - } else if note_pos > note_hi { - self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi)); - } - } - /// Make sure time range is within display - fn autozoom (&self) { - if self.time_lock().get() { - let time_len = self.get_time_len(); - let time_axis = self.get_time_axis(); - let time_zoom = self.get_time_zoom(); - loop { - let time_zoom = self.time_zoom().get(); - let time_area = time_axis * time_zoom; - if time_area > time_len { - let next_time_zoom = note_duration_prev(time_zoom); - if next_time_zoom <= 1 { - break - } - let next_time_area = time_axis * next_time_zoom; - if next_time_area >= time_len { - self.time_zoom().set(next_time_zoom); - } else { - break - } - } else if time_area < time_len { - let prev_time_zoom = note_duration_next(time_zoom); - if prev_time_zoom > 384 { - break - } - let prev_time_area = time_axis * prev_time_zoom; - if prev_time_area <= time_len { - self.time_zoom().set(prev_time_zoom); - } else { - break - } - } - } - if time_zoom != self.time_zoom().get() { - self.redraw() - } - } - //while time_len.div_ceil(time_zoom) > time_axis { - //println!("\r{time_len} {time_zoom} {time_axis}"); - //time_zoom = Note::next(time_zoom); - //} - //self.time_zoom().set(time_zoom); - } -} - -pub trait ClipsView: TracksView + ScenesView { - - fn view_scenes_clips <'a> (&'a self) - -> impl Draw + 'a - { - self.clips_size().of(wh_full(above( - wh_full(origin_se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), - Thunk::new(|to: &mut Tui|for ( - track_index, track, _, _ - ) in self.tracks_with_sizes() { - to.place(&w_exact(track.width as u16, - h_full(self.view_track_clips(track_index, track)))) - })))) - } - - fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw + 'a { - Thunk::new(move|to: &mut Tui|for ( - scene_index, scene, .. - ) in self.scenes_with_sizes() { - let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { - let clip = clip.read().unwrap(); - (format!(" ⏹ {}", &clip.name).into(), clip.color) - } else { - (" ⏹ -- ".into(), ItemTheme::G[32]) - }; - let fg = theme.lightest.rgb; - let mut outline = theme.base.rgb; - let bg = if self.selection().track() == Some(track_index) - && self.selection().scene() == Some(scene_index) - { - outline = theme.lighter.rgb; - theme.light.rgb - } else if self.selection().track() == Some(track_index) - || self.selection().scene() == Some(scene_index) - { - outline = theme.darkest.rgb; - theme.base.rgb - } else { - theme.dark.rgb - }; - let w = if self.selection().track() == Some(track_index) - && let Some(editor) = self.editor () - { - (editor.measure_width() as usize).max(24).max(track.width) - } else { - track.width - } as u16; - let y = if self.selection().scene() == Some(scene_index) - && let Some(editor) = self.editor () - { - (editor.measure_height() as usize).max(12) - } else { - Self::H_SCENE as usize - } as u16; - - to.place(&wh_exact(w, y, below( - wh_full(Outer(true, Style::default().fg(outline))), - wh_full(below( - below( - Tui::fg_bg(outline, bg, wh_full("")), - wh_full(origin_nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))), - ), - wh_full(when(self.selection().track() == Some(track_index) - && self.selection().scene() == Some(scene_index) - && self.is_editing(), self.editor()))))))); - }) - } - -} - -pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured { - - fn tracks_width_available (&self) -> u16 { - (self.measure_width() as u16).saturating_sub(40) - } - - /// Iterate over tracks with their corresponding sizes. - fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { - let _editor_width = self.editor().map(|e|e.measure_width()); - let _active_track = self.selection().track(); - let mut x = 0; - self.tracks().iter().enumerate().map_while(move |(index, track)|{ - let width = track.width.max(8); - if x + width < self.clips_size().w() as usize { - let data = (index, track, x, x + width); - x += width + Self::TRACK_SPACING; - Some(data) - } else { - None - } - }) - } - - fn view_track_names (&self, theme: ItemTheme) -> impl Draw { - let track_count = self.tracks().len(); - let scene_count = self.scenes().len(); - let selected = self.selection(); - let button = south( - button_3("t", "rack ", format!("{}{track_count}", selected.track() - .map(|track|format!("{track}/")).unwrap_or_default()), false), - button_3("s", "cene ", format!("{}{scene_count}", selected.scene() - .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); - let button_2 = south( - button_2("T", "+", false), - button_2("S", "+", false)); - view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb, - h_exact(2, Thunk::new(|to: &mut Tui|{ - for (index, track, x1, _x2) in self.tracks_with_sizes() { - to.place(&x_push(x1 as u16, w_exact(track_width(index, track), - Tui::bg(if selected.track() == Some(index) { - track.color.light.rgb - } else { - track.color.base.rgb - }, south(w_full(origin_nw(east( - format!("·t{index:02} "), - Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name)) - ))), ""))) ));}})))) - } - - fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw { - view_track_row_section(theme, - south(w_full(origin_w(button_2("o", "utput", false))), - Thunk::new(|to: &mut Tui|for port in self.midi_outs().iter() { - to.place(&w_full(origin_w(port.port_name()))); - })), - button_2("O", "+", false), - Tui::bg(theme.darker.rgb, origin_w(Thunk::new(|to: &mut Tui|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&w_exact(track_width(index, track), - origin_nw(h_full(iter_south(1, ||track.sequencer.midi_outs.iter(), - |port, index|Tui::fg(Rgb(255, 255, 255), - h_exact(1, Tui::bg(track.color.dark.rgb, w_full(origin_w( - format!("·o{index:02} {}", port.port_name())))))))))));}})))) - } - - fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw { - let mut h = 0u16; - for track in self.tracks().iter() { - h = h.max(track.sequencer.midi_ins.len() as u16); - } - let content = Thunk::new(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&wh_exact(track_width(index, track), h + 1, - origin_nw(south( - Tui::bg(track.color.base.rgb, - w_full(origin_w(east!( - either(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "), - either(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "), - either(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "), - )))), - iter_south(1, ||track.sequencer.midi_ins.iter(), - |port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb, - w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))); - }); - view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), - Tui::bg(theme.darker.rgb, origin_w(content))) - } - -} - -pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync { - /// Default scene height. - const H_SCENE: usize = 2; - /// Default editor height. - const H_EDITOR: usize = 15; - fn h_scenes (&self) -> u16; - fn w_side (&self) -> u16; - fn w_mid (&self) -> u16; - fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> { - let mut y = 0; - self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{ - let height = if self.selection().scene() == Some(s) && self.editor().is_some() { - 8 - } else { - Self::H_SCENE - }; - if y + height <= self.clips_size().h() as usize { - let data = (s, scene, y, y + height); - y += height; - Some(data) - } else { - None - } - }) - } - - fn view_scenes_names (&self) -> impl Draw { - w_exact(20, Thunk::new(|to: &mut Tui|for (index, scene, ..) in self.scenes_with_sizes() { - to.place(&self.view_scene_name(index, scene)); - })) - } - - fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw + 'a { - let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() { - 7 - } else { - Self::H_SCENE as u16 - }; - let bg = if self.selection().scene() == Some(index) { - scene.color.light.rgb - } else { - scene.color.base.rgb - }; - let a = w_full(origin_w(east(format!("·s{index:02} "), - Tui::fg(Tui::g(255), Tui::bold(true, &scene.name))))); - let b = when(self.selection().scene() == Some(index) && self.is_editing(), - wh_full(origin_nw(south( - self.editor().as_ref().map(|e|e.clip_status()), - self.editor().as_ref().map(|e|e.edit_status()))))); - wh_exact(20, h, Tui::bg(bg, origin_nw(south(a, b)))) - } - -} - -/// May create new MIDI input ports. -pub trait AddMidiIn { - fn midi_in_add (&mut self) -> Usually<()>; -} - -/// May create new MIDI output ports. -pub trait AddMidiOut { - fn midi_out_add (&mut self) -> Usually<()>; -} - -pub trait RegisterPorts: HasJack<'static> { - /// Register a MIDI input port. - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register a MIDI output port. - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio input port. - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio output port. - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; -} - -pub trait JackPort: HasJack<'static> { - - type Port: PortSpec + Default; - - type Pair: PortSpec + Default; - - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized; - - fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { - jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) - .map_err(|e|e.into()) - } - - fn port_name (&self) -> &Arc; - - fn connections (&self) -> &[Connect]; - - fn port (&self) -> &Port; - - fn port_mut (&mut self) -> &mut Port; - - fn into_port (self) -> Port where Self: Sized; - - fn close (self) -> Usually<()> where Self: Sized { - let jack = self.jack().clone(); - Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) - } - - fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { - self.with_client(|c|c.ports(re_name, re_type, flags)) - } - - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - - fn port_by_name (&self, name: impl AsRef) -> Option> { - self.with_client(|c|c.port_by_name(name.as_ref())) - } - - fn connect_to_matching <'k> (&'k self) -> Usually<()> { - for connect in self.connections().iter() { - match &connect.name { - Some(Exact(name)) => { - *connect.status.write().unwrap() = self.connect_exact(name)?; - }, - Some(RegExp(re)) => { - *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; - }, - _ => {}, - }; - } - Ok(()) - } - - fn connect_exact <'k> (&'k self, name: &str) -> - Usually, Arc, ConnectStatus)>> - { - self.with_client(move|c|{ - let mut status = vec![]; - for port in c.ports(None, None, PortFlags::empty()).iter() { - if port.as_str() == &*name { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected { - break - } - } - } - } - Ok(status) - }) - } - - fn connect_regexp <'k> ( - &'k self, re: &str, scope: Option - ) -> Usually, Arc, ConnectStatus)>> { - self.with_client(move|c|{ - let mut status = vec![]; - let ports = c.ports(Some(&re), None, PortFlags::empty()); - for port in ports.iter() { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected && scope == Some(One) { - break - } - } - } - Ok(status) - }) - } - - /** Connect to a matching port by name. */ - fn connect_to_name (&self, name: impl AsRef) -> Usually { - self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { - self.connect_to_unowned(port) - } else { - Ok(Missing) - }) - } - - /** Connect to a matching port by reference. */ - fn connect_to_unowned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } - - /** Connect to an owned matching port by reference. */ - fn connect_to_owned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } - -} diff --git a/app/tek_type.rs b/app/tek_type.rs deleted file mode 100644 index fd2c7d9d..00000000 --- a/app/tek_type.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::*; - -pub type MidiData = - Vec>; - -pub type ClipPool = - Vec>>; - -pub type CollectedMidiInput<'a> = - Vec, MidiError>)>>; - -pub type SceneWith<'a, T> = - (usize, &'a Scene, usize, usize, T); - -pub type MidiSample = - (Option, Arc>); - -/// Collection of interaction modes. -pub type Modes = Arc, Arc>>>>>; - -/// Collection of input bindings. -pub type Binds = Arc, Bind>>>>; - -/// Collection of view definitions. -pub type Views = Arc, Arc>>>; diff --git a/app/tick.rs b/app/tick.rs new file mode 100644 index 00000000..c6bdd169 --- /dev/null +++ b/app/tick.rs @@ -0,0 +1,168 @@ +pub trait HasClock: AsRef + AsMut { + fn clock (&self) -> &Clock { self.as_ref() } + fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } +} + + +/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) +/// +/// ``` +/// let _ = tek::Timebase::default(); +/// ``` +#[derive(Debug, Clone)] pub struct Timebase { + /// Audio samples per second + pub sr: SampleRate, + /// MIDI beats per minute + pub bpm: Bpm, + /// MIDI ticks per beat + pub ppq: Ppq, +} + +/// Iterator that emits subsequent ticks within a range. +/// +/// ``` +/// let iter = tek::Ticker::default(); +/// ``` +#[derive(Debug, Default)] pub struct Ticker { + pub spp: f64, + pub sample: usize, + pub start: usize, + pub end: usize, +} + +/// A point in time in all time scales (microsecond, sample, MIDI pulse) +/// +/// ``` +/// let _ = tek::Moment::default(); +/// ``` +#[derive(Debug, Default, Clone)] pub struct Moment { + pub timebase: Arc, + /// Current time in microseconds + pub usec: Microsecond, + /// Current time in audio samples + pub sample: SampleCount, + /// Current time in MIDI pulses + pub pulse: Pulse, +} + +/// +/// ``` +/// let _ = tek::Moment2::default(); +/// ``` +#[derive(Debug, Clone, Default)] pub enum Moment2 { + #[default] None, + Zero, + Usec(Microsecond), + Sample(SampleCount), + Pulse(Pulse), +} + +/// MIDI resolution in PPQ (pulses per quarter note) +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64); + +/// Timestamp in MIDI pulses +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64); + +/// Tempo in beats per minute +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64); + +/// Quantization setting for launching clips +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64); + +/// Quantization setting for notes +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64); + +/// Timestamp in audio samples +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64); + +/// Audio sample rate in Hz (samples per second) +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64); + +/// Timestamp in microseconds +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64); + +/// The source of time. +/// +/// ``` +/// let clock = tek::Clock::default(); +/// ``` +#[derive(Clone, Default)] pub struct Clock { + /// JACK transport handle. + pub transport: Arc>, + /// Global temporal resolution (shared by [Moment] fields) + pub timebase: Arc, + /// Current global sample and usec (monotonic from JACK clock) + pub global: Arc, + /// Global sample and usec at which playback started + pub started: Arc>>, + /// Playback offset (when playing not from start) + pub offset: Arc, + /// Current playhead position + pub playhead: Arc, + /// Note quantization factor + pub quant: Arc, + /// Launch quantization factor + pub sync: Arc, + /// Size of buffer in samples + pub chunk: Arc, + // Cache of formatted strings + pub view_cache: Arc>, + /// For syncing the clock to an external source + #[cfg(feature = "port")] pub midi_in: Arc>>, + /// For syncing other devices to this clock + #[cfg(feature = "port")] pub midi_out: Arc>>, + /// For emitting a metronome + #[cfg(feature = "port")] pub click_out: Arc>>, +} + +/// A unit of time, represented as an atomic 64-bit float. +/// +/// According to https://stackoverflow.com/a/873367, as per IEEE754, +/// every integer between 1 and 2^53 can be represented exactly. +/// This should mean that, even at 192kHz sampling rate, over 1 year of audio +/// can be clocked in microseconds with f64 without losing precision. +pub trait TimeUnit: InteriorMutable {} + +/// Contains memoized renders of clock values. +/// +/// Performance optimization. +#[derive(Debug)] pub struct ClockView { + pub sr: Memo, String>, + pub buf: Memo, String>, + pub lat: Memo, String>, + pub bpm: Memo, String>, + pub beat: Memo, String>, + pub time: Memo, String>, +} From 915e13aec8dae6c9110f369a3d10eac472d89145 Mon Sep 17 00:00:00 2001 From: okay stopped screaming Date: Sat, 21 Mar 2026 22:54:54 +0200 Subject: [PATCH 3/7] wip: nmoralize --- Cargo.toml | 4 ++-- {app => src}/.scratch.rs | 0 {app => src}/arrange.rs | 0 {app => src}/browse.rs | 0 {app => src}/connect.rs | 0 {app => src}/device.rs | 0 {app => src}/mix.rs | 0 {app => src}/plugin.rs | 0 {app => src}/sample.rs | 0 {app => src}/sequence.rs | 0 {app => src}/tek.edn | 0 {app => src}/tek.rs | 0 {app => src}/tek_impls.rs | 0 {app => src}/tick.rs | 0 14 files changed, 2 insertions(+), 2 deletions(-) rename {app => src}/.scratch.rs (100%) rename {app => src}/arrange.rs (100%) rename {app => src}/browse.rs (100%) rename {app => src}/connect.rs (100%) rename {app => src}/device.rs (100%) rename {app => src}/mix.rs (100%) rename {app => src}/plugin.rs (100%) rename {app => src}/sample.rs (100%) rename {app => src}/sequence.rs (100%) rename {app => src}/tek.edn (100%) rename {app => src}/tek.rs (100%) rename {app => src}/tek_impls.rs (100%) rename {app => src}/tick.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index fd3888d8..299ba0dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,11 +4,11 @@ edition = "2024" version = "0.3.0" [lib] -path = "app/tek.rs" +path = "src/tek.rs" [[bin]] name = "tek" -path = "app/tek.rs" +path = "src/tek.rs" [target.'cfg(target_os = "linux")'] rustflags = ["-C", "link-arg=-fuse-ld=mold"] diff --git a/app/.scratch.rs b/src/.scratch.rs similarity index 100% rename from app/.scratch.rs rename to src/.scratch.rs diff --git a/app/arrange.rs b/src/arrange.rs similarity index 100% rename from app/arrange.rs rename to src/arrange.rs diff --git a/app/browse.rs b/src/browse.rs similarity index 100% rename from app/browse.rs rename to src/browse.rs diff --git a/app/connect.rs b/src/connect.rs similarity index 100% rename from app/connect.rs rename to src/connect.rs diff --git a/app/device.rs b/src/device.rs similarity index 100% rename from app/device.rs rename to src/device.rs diff --git a/app/mix.rs b/src/mix.rs similarity index 100% rename from app/mix.rs rename to src/mix.rs diff --git a/app/plugin.rs b/src/plugin.rs similarity index 100% rename from app/plugin.rs rename to src/plugin.rs diff --git a/app/sample.rs b/src/sample.rs similarity index 100% rename from app/sample.rs rename to src/sample.rs diff --git a/app/sequence.rs b/src/sequence.rs similarity index 100% rename from app/sequence.rs rename to src/sequence.rs diff --git a/app/tek.edn b/src/tek.edn similarity index 100% rename from app/tek.edn rename to src/tek.edn diff --git a/app/tek.rs b/src/tek.rs similarity index 100% rename from app/tek.rs rename to src/tek.rs diff --git a/app/tek_impls.rs b/src/tek_impls.rs similarity index 100% rename from app/tek_impls.rs rename to src/tek_impls.rs diff --git a/app/tick.rs b/src/tick.rs similarity index 100% rename from app/tick.rs rename to src/tick.rs From 35197fb826ad7f00b7fbd2494d335dd6fc59f13d Mon Sep 17 00:00:00 2001 From: okay stopped screaming Date: Sat, 21 Mar 2026 23:12:30 +0200 Subject: [PATCH 4/7] wip: nermalize --- src/arrange.rs | 586 +++++++- src/browse.rs | 254 ++++ src/clock.rs | 702 +++++++++ src/connect.rs | 306 ---- src/device.rs | 583 ++++++++ src/mix.rs | 56 + src/plugin.rs | 150 ++ src/sample.rs | 549 +++++++ src/sequence.rs | 820 +++++++++++ src/tek.rs | 1607 +++++++++++++-------- src/tek_impls.rs | 3586 ---------------------------------------------- src/tick.rs | 168 --- 12 files changed, 4649 insertions(+), 4718 deletions(-) create mode 100644 src/clock.rs delete mode 100644 src/connect.rs delete mode 100644 src/tek_impls.rs delete mode 100644 src/tick.rs diff --git a/src/arrange.rs b/src/arrange.rs index 0034912d..45417748 100644 --- a/src/arrange.rs +++ b/src/arrange.rs @@ -1,44 +1,7 @@ - -/// Represents the current user selection in the arranger -#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { - #[default] - /// Nothing is selected - Nothing, - /// The whole mix is selected - Mix, - /// A MIDI input is selected. - Input(usize), - /// A MIDI output is selected. - Output(usize), - /// A scene is selected. - #[cfg(feature = "scene")] Scene(usize), - /// A track is selected. - #[cfg(feature = "track")] Track(usize), - /// A clip (track × scene) is selected. - #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, - /// A track's MIDI input connection is selected. - #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, - /// A track's MIDI output connection is selected. - #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, - /// A track device slot is selected. - #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, -} - -/// A scene consists of a set of clips to play together. -/// -/// ``` -/// let scene: tek::Scene = Default::default(); -/// let _ = scene.pulses(); -/// let _ = scene.is_playing(&[]); -/// ``` -#[derive(Debug, Default)] pub struct Scene { - /// Name of scene - pub name: Arc, - /// Identifying color of scene - pub color: ItemTheme, - /// Clips in scene, one per track - pub clips: Vec>>>, -} +use ::std::sync::{Arc, RwLock}; +use ::tengri::{space::east, color::ItemTheme}; +use ::tengri::{draw::*, term::*}; +use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput}; /// Arranger. /// @@ -155,6 +118,47 @@ pub trait ClipsView: TracksView + ScenesView { } +/// Represents the current user selection in the arranger +#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { + #[default] + /// Nothing is selected + Nothing, + /// The whole mix is selected + Mix, + /// A MIDI input is selected. + Input(usize), + /// A MIDI output is selected. + Output(usize), + /// A scene is selected. + #[cfg(feature = "scene")] Scene(usize), + /// A track is selected. + #[cfg(feature = "track")] Track(usize), + /// A clip (track × scene) is selected. + #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, + /// A track's MIDI input connection is selected. + #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, + /// A track's MIDI output connection is selected. + #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, + /// A track device slot is selected. + #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, +} + +/// A scene consists of a set of clips to play together. +/// +/// ``` +/// let scene: tek::Scene = Default::default(); +/// let _ = scene.pulses(); +/// let _ = scene.is_playing(&[]); +/// ``` +#[derive(Debug, Default)] pub struct Scene { + /// Name of scene + pub name: Arc, + /// Identifying color of scene + pub color: ItemTheme, + /// Clips in scene, one per track + pub clips: Vec>>>, +} + pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured { fn tracks_width_available (&self) -> u16 { @@ -414,3 +418,503 @@ pub trait HasTrack: AsRefOpt + AsMutOpt { self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) } } + impl_as_ref!(Vec: |self: App| self.project.as_ref()); + impl_as_mut!(Vec: |self: App| self.project.as_mut()); + #[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt()); + #[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt()); + impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } + impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } } + + impl HasWidth for Track { + const MIN_WIDTH: usize = 9; + fn width_inc (&mut self) { self.width += 1; } + fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } + } + + impl Track { + /// Create a new track with only the default [Sequencer]. + pub fn new ( + name: &impl AsRef, + color: Option, + jack: &Jack<'static>, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[Connect], + midi_to: &[Connect], + ) -> Usually { + Ok(Self { + name: name.as_ref().into(), + color: color.unwrap_or_default(), + sequencer: Sequencer::new(format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to)?, + ..Default::default() + }) + } + pub fn audio_ins (&self) -> &[AudioInput] { + self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() + } + pub fn audio_outs (&self) -> &[AudioOutput] { + self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() + } + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + pub fn per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Draw + 'a { + iter(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + let width = (x2 - x1) as u16; + iter_east(x1 as u16, width, w_exact(width, Tui::fg_bg( + track.color.lightest.rgb, + track.color.base.rgb, + callback(index, track))))}) + } + /// Create a new track connecting the [Sequencer] to a [Sampler]. + #[cfg(feature = "sampler")] pub fn new_with_sampler ( + name: &impl AsRef, + color: Option, + jack: &Jack<'static>, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[Connect], + midi_to: &[Connect], + audio_from: &[&[Connect];2], + audio_to: &[&[Connect];2], + ) -> Usually { + let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; + let client_name = jack.with_client(|c|c.name().to_string()); + let port_name = track.sequencer.midi_outs[0].port_name(); + let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; + track.devices.push(Device::Sampler(Sampler::new( + jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to + )?)); + Ok(track) + } + #[cfg(feature = "sampler")] pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { + for device in self.devices.iter() { + match device { + Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, + _ => {} + } + } + None + } + #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { + for device in self.devices.iter_mut() { + match device { + Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, + _ => {} + } + } + None + } + } + + pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Draw + 'a { + per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track)))) + } + + pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Draw + 'a { + origin_x(Tui::bg(Reset, iter(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + let width = (x2 - x1) as u16; + iter_east(x1 as u16, width, w_exact(width, Tui::fg_bg( + track.color.lightest.rgb, + track.color.base.rgb, + callback(index, track))))}))) + } + #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt()); + #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); + #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene()); + #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut()); + impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } + impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } + impl ScenesView for App { + fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(self.w_side()) } + fn w_side (&self) -> u16 { 20 } + fn h_scenes (&self) -> u16 { (self.measure_height() as u16).saturating_sub(20) } + } + impl Scene { + /// 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: &[Track]) -> 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.sequencer().play_clip() { + *clip.read().unwrap() == *c.read().unwrap() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) + } + pub fn clip (&self, index: usize) -> Option<&Arc>> { + match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } + } + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + } + +impl Selection { + pub fn describe ( + &self, + #[cfg(feature = "track")] tracks: &[Track], + #[cfg(feature = "scene")] scenes: &[Scene], + ) -> Arc { + use Selection::*; + format!("{}", match self { + Mix => "Everything".to_string(), + #[cfg(feature = "scene")] Scene(s) => + scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), + #[cfg(feature = "track")] Track(t) => + tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), + TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { + (Some(_), Some(s)) => match s.clip(*track) { + Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), + None => format!("T{track} S{scene}: Empty") + }, + _ => format!("T{track} S{scene}: Empty"), + }, + _ => todo!() + }).into() + } + #[cfg(feature = "scene")] pub fn scene (&self) -> Option { + use Selection::*; + match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } + } + #[cfg(feature = "scene")] pub fn select_scene (&self, scene_count: usize) -> Self { + use Selection::*; + match self { + Mix | Track(_) => Scene(0), + Scene(s) => Scene((s + 1) % scene_count), + TrackClip { scene, .. } => Track(*scene), + _ => todo!(), + } + } + #[cfg(feature = "scene")] pub fn select_scene_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Scene(0), + Track(t) => TrackClip { track: *t, scene: 0 }, + Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, + TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, + _ => todo!() + } + } + #[cfg(feature = "scene")] pub fn select_scene_prev (&self) -> Self { + use Selection::*; + match self { + Mix | Scene(0) => Mix, + Scene(s) => Scene(s - 1), + Track(t) => Track(*t), + TrackClip { track, scene: 0 } => Track(*track), + TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, + _ => todo!() + } + } + #[cfg(feature = "track")] pub fn track (&self) -> Option { + use Selection::*; + if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { + Some(*track) + } else { + None + } + } + #[cfg(feature = "track")] pub fn select_track (&self, track_count: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(_) => Mix, + Track(t) => Track((t + 1) % track_count), + TrackClip { track, .. } => Track(*track), + _ => todo!(), + } + } + #[cfg(feature = "track")] pub fn select_track_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(s) => TrackClip { track: 0, scene: *s }, + Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, + TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, + _ => todo!() + } + } + #[cfg(feature = "track")] pub fn select_track_prev (&self) -> Self { + use Selection::*; + match self { + Mix => Mix, + Scene(s) => Scene(*s), + Track(0) => Mix, + Track(t) => Track(t - 1), + TrackClip { track: 0, scene } => Scene(*scene), + TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, + _ => todo!() + } + } +} + impl_has!(Jack<'static>: |self: Arrangement| self.jack); + impl_has!(Measure: |self: Arrangement| self.size); + impl_has!(Vec: |self: Arrangement| self.tracks); + impl_has!(Vec: |self: Arrangement| self.scenes); + impl_has!(Vec: |self: Arrangement| self.midi_ins); + impl_has!(Vec: |self: Arrangement| self.midi_outs); + impl_has!(Clock: |self: Arrangement| self.clock); + impl_has!(Selection: |self: Arrangement| self.selection); + impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); + impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); + impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); + impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); + impl Arrangement { + /// Create a new arrangement. + pub fn new ( + jack: &Jack<'static>, + name: Option>, + clock: Clock, + tracks: Vec, + scenes: Vec, + midi_ins: Vec, + midi_outs: Vec, + ) -> Self { + Self { + clock, tracks, scenes, midi_ins, midi_outs, + jack: jack.clone(), + name: name.unwrap_or_default(), + color: ItemTheme::random(), + selection: Selection::TrackClip { track: 0, scene: 0 }, + ..Default::default() + } + } + /// Width of display + pub fn w (&self) -> u16 { + self.size.w() as u16 + } + /// Width allocated for sidebar. + pub fn w_sidebar (&self, is_editing: bool) -> u16 { + self.w() / if is_editing { 16 } else { 8 } as u16 + } + /// Width available to display tracks. + pub fn w_tracks_area (&self, is_editing: bool) -> u16 { + self.w().saturating_sub(self.w_sidebar(is_editing)) + } + /// Height of display + pub fn h (&self) -> u16 { + self.size.h() as u16 + } + /// Height taken by visible device slots. + pub fn h_devices (&self) -> u16 { + 2 + //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) + } + /// Add multiple tracks + #[cfg(feature = "track")] pub fn tracks_add ( + &mut self, + count: usize, width: Option, + mins: &[Connect], mouts: &[Connect], + ) -> Usually<()> { + 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 track = self.track_add(None, Some(color), mins, mouts)?.1; + if let Some(width) = width { + track.width = width; + } + } + Ok(()) + } + /// Add a track + #[cfg(feature = "track")] pub fn track_add ( + &mut self, + name: Option<&str>, color: Option, + mins: &[Connect], mouts: &[Connect], + ) -> Usually<(usize, &mut Track)> { + let name: Arc = name.map_or_else( + ||format!("trk{:02}", self.track_last).into(), + |x|x.to_string().into() + ); + self.track_last += 1; + let track = Track { + width: (name.len() + 2).max(12), + color: color.unwrap_or_else(ItemTheme::random), + sequencer: Sequencer::new( + &format!("{name}"), + self.jack(), + Some(self.clock()), + None, + mins, + mouts + )?, + name, + ..Default::default() + }; + self.tracks_mut().push(track); + let len = self.tracks().len(); + let index = len - 1; + for scene in self.scenes_mut().iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); + } + } + Ok((index, &mut self.tracks_mut()[index])) + } + #[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { + south( + h_exact(1, self.view_inputs_header()), + Thunk::new(|to: &mut Tui|{ + for (index, port) in self.midi_ins().iter().enumerate() { + to.place(&x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port)))) + } + }) + ) + } + #[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Draw + '_ { + east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), + west(w_exact(4, button_2("I", "+", false)), Thunk::new(move|to: &mut Tui|for (_index, track, x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] + to.place(&x_push(x1 as u16, Tui::bg(track.color.dark.rgb, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), + either(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), + either(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), + )))))) + }))) + } + #[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Draw { + east(w_exact(20, origin_w(east(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), + west(w_exact(4, ()), Thunk::new(move|to: &mut Tui|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] + to.place(&Tui::bg(track.color.darker.rgb, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), + either(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), + either(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), + ))))) + }))) + } + #[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { + let mut h = 1; + for output in self.midi_outs().iter() { + h += 1 + output.connections.len(); + } + let h = h as u16; + let list = south( + h_exact(1, w_full(origin_w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), + h_exact(h - 1, wh_full(origin_nw(Thunk::new(|to: &mut Tui|{ + for (_index, port) in self.midi_outs().iter().enumerate() { + to.place(&h_exact(1,w_full(east( + origin_w(east(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), + w_full(origin_e(format!("{}/{} ", + port.port().get_connections().len(), + port.connections.len()))))))); + for (index, conn) in port.connections.iter().enumerate() { + to.place(&h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info()))))); + } + } + }))))); + h_exact(h, view_track_row_section(theme, list, button_2("O", "+", false), + Tui::bg(theme.darker.rgb, origin_w(w_full( + Thunk::new(|to: &mut Tui|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&w_exact(track_width(index, track), + Thunk::new(|to: &mut Tui|{ + to.place(&h_exact(1, origin_w(east( + either(true, Tui::fg(Green, "play "), "play "), + either(false, Tui::fg(Yellow, "solo "), "solo "), + )))); + for (_index, port) in self.midi_outs().iter().enumerate() { + to.place(&h_exact(1, origin_w(east( + either(true, Tui::fg(Green, " ● "), " · "), + either(false, Tui::fg(Yellow, " ● "), " · "), + )))); + for (_index, _conn) in port.connections.iter().enumerate() { + to.place(&h_exact(1, w_full(""))); + } + }})))}})))))) + } + #[cfg(feature = "track")] pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { + let mut h = 2u16; + for track in self.tracks().iter() { + h = h.max(track.devices.len() as u16 * 2); + } + view_track_row_section(theme, + button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), + button_2("D", "+", false), + Thunk::new(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&wh_exact(track_width(index, track), h + 1, + Tui::bg(track.color.dark.rgb, origin_nw(iter_south(2, move||0..h, + |_, _index|wh_exact(track.width as u16, 2, + Tui::fg_bg( + ItemTheme::G[32].lightest.rgb, + ItemTheme::G[32].dark.rgb, + origin_nw(format!(" · {}", "--"))))))))); + })) + } + /// Put a clip in a slot + #[cfg(feature = "clip")] pub fn clip_put ( + &mut self, track: usize, scene: usize, clip: Option>> + ) -> Option>> { + let old = self.scenes[scene].clips[track].clone(); + self.scenes[scene].clips[track] = clip; + old + } + /// Change the color of a clip, returning the previous one + #[cfg(feature = "clip")] pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) + -> Option + { + self.scenes[scene].clips[track].as_ref().map(|clip|{ + let mut clip = clip.write().unwrap(); + let old = clip.color.clone(); + clip.color = color.clone(); + panic!("{color:?} {old:?}"); + //old + }) + } + /// Toggle looping for the active clip + #[cfg(feature = "clip")] pub fn toggle_loop (&mut self) { + if let Some(clip) = self.selected_clip() { + clip.write().unwrap().toggle_loop() + } + } + /// Get the first sampler of the active track + #[cfg(feature = "sampler")] pub fn sampler (&self) -> Option<&Sampler> { + self.selected_track()?.sampler(0) + } + /// Get the first sampler of the active track + #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { + self.selected_track_mut()?.sampler_mut(0) + } + } + impl ScenesView for Arrangement { + fn h_scenes (&self) -> u16 { + (self.measure_height() as u16).saturating_sub(20) + } + fn w_side (&self) -> u16 { + (self.measure_width() as u16 * 2 / 10).max(20) + } + fn w_mid (&self) -> u16 { + (self.measure_width() as u16).saturating_sub(2 * self.w_side()).max(40) + } + } + impl HasClipsSize for Arrangement { + fn clips_size (&self) -> &Measure { &self.size_inner } + } diff --git a/src/browse.rs b/src/browse.rs index ccf7e37c..f9ca478b 100644 --- a/src/browse.rs +++ b/src/browse.rs @@ -1,3 +1,7 @@ +use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; +use crate::sequence::MidiClip; +use crate::sample::Sample; + /// Browses for files to load/save. /// /// ``` @@ -95,3 +99,253 @@ pub struct PoolView<'a>(pub &'a Pool); /// Editing the number of ticks Tick, } +has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); +impl_has_clips!(|self: Pool|self.clips); +impl_from!(Pool: |clip:&Arc>|{ + let model = Self::default(); + model.clips.write().unwrap().push(clip.clone()); + model.clip.store(1, Relaxed); + model +}); +impl_default!(Pool: Self { + browse: None, + clip: 0.into(), + clips: Arc::from(RwLock::from(vec![])), + mode: None, + samples: Arc::from(RwLock::from(vec![])), + visible: true, +}); +impl Pool { + pub fn clip_index (&self) -> usize { + self.clip.load(Relaxed) + } + pub fn set_clip_index (&self, value: usize) { + self.clip.store(value, Relaxed); + } + pub fn mode (&self) -> &Option { + &self.mode + } + pub fn mode_mut (&mut self) -> &mut Option { + &mut self.mode + } + pub fn begin_clip_length (&mut self) { + let length = self.clips()[self.clip_index()].read().unwrap().length; + *self.mode_mut() = Some(PoolMode::Length( + self.clip_index(), + length, + ClipLengthFocus::Bar + )); + } + pub fn begin_clip_rename (&mut self) { + let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); + *self.mode_mut() = Some(PoolMode::Rename( + self.clip_index(), + name + )); + } + pub fn begin_import (&mut self) -> Usually<()> { + *self.mode_mut() = Some(PoolMode::Import( + self.clip_index(), + Browse::new(None)? + )); + Ok(()) + } + pub fn begin_export (&mut self) -> Usually<()> { + *self.mode_mut() = Some(PoolMode::Export( + self.clip_index(), + Browse::new(None)? + )); + Ok(()) + } + pub fn new_clip (&self) -> MidiClip { + MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) + } + pub fn cloned_clip (&self) -> MidiClip { + let index = self.clip_index(); + let mut clip = self.clips()[index].read().unwrap().duplicate(); + clip.color = ItemTheme::random_near(clip.color, 0.25); + clip + } + pub fn add_new_clip (&self) -> (usize, Arc>) { + let clip = Arc::new(RwLock::new(self.new_clip())); + let index = { + let mut clips = self.clips.write().unwrap(); + clips.push(clip.clone()); + clips.len().saturating_sub(1) + }; + self.clip.store(index, Relaxed); + (index, clip) + } + pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { + let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); + if let Some(index) = index { + self.clips.write().unwrap().remove(index); + return true + } + false + } +} +impl ClipLengthFocus { + pub fn next (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } + } + pub fn prev (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } + } +} +impl ClipLength { + pub fn _new (pulses: usize, focus: Option) -> Self { + Self { ppq: PPQ, bpb: 4, pulses, focus } + } + pub fn bars (&self) -> usize { + self.pulses / (self.bpb * self.ppq) + } + pub fn beats (&self) -> usize { + (self.pulses % (self.bpb * self.ppq)) / self.ppq + } + pub fn ticks (&self) -> usize { + self.pulses % self.ppq + } + pub fn bars_string (&self) -> Arc { + format!("{}", self.bars()).into() + } + pub fn beats_string (&self) -> Arc { + format!("{}", self.beats()).into() + } + pub fn ticks_string (&self) -> Arc { + format!("{:>02}", self.ticks()).into() + } +} + +impl Pool { + fn _todo_usize_ (&self) -> usize { todo!() } + fn _todo_bool_ (&self) -> bool { todo!() } + fn _todo_clip_ (&self) -> MidiClip { todo!() } + fn _todo_path_ (&self) -> PathBuf { todo!() } + fn _todo_color_ (&self) -> ItemColor { todo!() } + fn _todo_str_ (&self) -> Arc { todo!() } + fn _clip_new (&self) -> MidiClip { self.new_clip() } + fn _clip_cloned (&self) -> MidiClip { self.cloned_clip() } + fn _clip_index_current (&self) -> usize { 0 } + fn _clip_index_after (&self) -> usize { 0 } + fn _clip_index_previous (&self) -> usize { 0 } + fn _clip_index_next (&self) -> usize { 0 } + fn _color_random (&self) -> ItemColor { ItemColor::random() } +} + +impl<'a> PoolView<'a> { + fn tui (&self) -> impl Draw { + let Self(pool) = self; + //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); + //let on_bg = |x|x;//below(Repeat(" "), Tui::bg(color.darkest.rgb, x)); + //let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); + //let height = pool.clips.read().unwrap().len() as u16; + w_exact(20, h_full(origin_n(iter( + ||pool.clips().clone().into_iter(), + move|clip: Arc>, i: usize|{ + let item_height = 1; + let item_offset = i as u16 * item_height; + let selected = i == pool.clip_index(); + let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); + let bg = if selected { color.light.rgb } else { color.base.rgb }; + let fg = color.lightest.rgb; + let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; + let length = if false { String::default() } else { format!("{length} ") }; + h_exact(1, iter_south(item_offset, item_height, Tui::bg(bg, below!( + w_full(origin_w(Tui::fg(fg, Tui::bold(selected, name)))), + w_full(origin_e(Tui::fg(fg, Tui::bold(selected, length)))), + w_full(origin_w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), + w_full(origin_e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), + )))) + })))) + } +} +impl ClipLength { + fn tui (&self) -> impl Draw { + use ClipLengthFocus::*; + let bars = ||self.bars_string(); + let beats = ||self.beats_string(); + let ticks = ||self.ticks_string(); + match self.focus { + None => east!(" ", bars(), ".", beats(), ".", ticks()), + Some(Bar) => east!("[", bars(), "]", beats(), ".", ticks()), + Some(Beat) => east!(" ", bars(), "[", beats(), "]", ticks()), + Some(Tick) => east!(" ", bars(), ".", beats(), "[", ticks()), + } + } +} + impl Browse { + pub fn new (cwd: Option) -> Usually { + let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; + let mut dirs = vec![]; + let mut files = vec![]; + for entry in std::fs::read_dir(&cwd)? { + let entry = entry?; + let name = entry.file_name(); + let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); + let meta = entry.metadata()?; + if meta.is_dir() { + dirs.push((name, format!("📁 {decoded}"))); + } else if meta.is_file() { + files.push((name, format!("📄 {decoded}"))); + } + } + Ok(Self { cwd, dirs, files, ..Default::default() }) + } + pub fn chdir (&self) -> Usually { Self::new(Some(self.path())) } + pub fn len (&self) -> usize { self.dirs.len() + self.files.len() } + pub fn is_dir (&self) -> bool { self.index < self.dirs.len() } + pub fn is_file (&self) -> bool { self.index >= self.dirs.len() } + pub fn path (&self) -> PathBuf { + self.cwd.join(if self.is_dir() { + &self.dirs[self.index].0 + } else if self.is_file() { + &self.files[self.index - self.dirs.len()].0 + } else { + unreachable!() + }) + } + fn _todo_stub_path_buf (&self) -> PathBuf { todo!() } + fn _todo_stub_usize (&self) -> usize { todo!() } + fn _todo_stub_arc_str (&self) -> Arc { todo!() } + } + impl Browse { + fn tui (&self) -> impl Draw { + iter_south(1, ||EntriesIterator { + offset: 0, + index: 0, + length: self.dirs.len() + self.files.len(), + browser: self, + }, |entry, _index|w_full(origin_w(entry))) + } + } + impl<'a> Iterator for EntriesIterator<'a> { + type Item = Modify<&'a str>; + fn next (&mut self) -> Option { + let dirs = self.browser.dirs.len(); + let files = self.browser.files.len(); + let index = self.index; + if self.index < dirs { + self.index += 1; + Some(Tui::bold(true, self.browser.dirs[index].1.as_str())) + } else if self.index < dirs + files { + self.index += 1; + Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str())) + } else { + None + } + } + } + impl PartialEq for BrowseTarget { + fn eq (&self, other: &Self) -> bool { + match self { + Self::ImportSample(_) => false, + Self::ExportSample(_) => false, + Self::ImportClip(_) => false, + Self::ExportClip(_) => false, + #[allow(unused)] t => matches!(other, t) + } + } + } diff --git a/src/clock.rs b/src/clock.rs new file mode 100644 index 00000000..2d78af28 --- /dev/null +++ b/src/clock.rs @@ -0,0 +1,702 @@ +use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; +use ::atomic_float::AtomicF64; +use ::tengri::{draw::*, term::*}; + +pub trait HasClock: AsRef + AsMut { + fn clock (&self) -> &Clock { self.as_ref() } + fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } +} + +/// The source of time. +/// +/// ``` +/// let clock = tek::Clock::default(); +/// ``` +#[derive(Clone, Default)] pub struct Clock { + /// JACK transport handle. + pub transport: Arc>, + /// Global temporal resolution (shared by [Moment] fields) + pub timebase: Arc, + /// Current global sample and usec (monotonic from JACK clock) + pub global: Arc, + /// Global sample and usec at which playback started + pub started: Arc>>, + /// Playback offset (when playing not from start) + pub offset: Arc, + /// Current playhead position + pub playhead: Arc, + /// Note quantization factor + pub quant: Arc, + /// Launch quantization factor + pub sync: Arc, + /// Size of buffer in samples + pub chunk: Arc, + // Cache of formatted strings + pub view_cache: Arc>, + /// For syncing the clock to an external source + #[cfg(feature = "port")] pub midi_in: Arc>>, + /// For syncing other devices to this clock + #[cfg(feature = "port")] pub midi_out: Arc>>, + /// For emitting a metronome + #[cfg(feature = "port")] pub click_out: Arc>>, +} + +/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) +/// +/// ``` +/// let _ = tek::Timebase::default(); +/// ``` +#[derive(Debug, Clone)] pub struct Timebase { + /// Audio samples per second + pub sr: SampleRate, + /// MIDI beats per minute + pub bpm: Bpm, + /// MIDI ticks per beat + pub ppq: Ppq, +} + +/// Iterator that emits subsequent ticks within a range. +/// +/// ``` +/// let iter = tek::Ticker::default(); +/// ``` +#[derive(Debug, Default)] pub struct Ticker { + pub spp: f64, + pub sample: usize, + pub start: usize, + pub end: usize, +} + +/// A point in time in all time scales (microsecond, sample, MIDI pulse) +/// +/// ``` +/// let _ = tek::Moment::default(); +/// ``` +#[derive(Debug, Default, Clone)] pub struct Moment { + pub timebase: Arc, + /// Current time in microseconds + pub usec: Microsecond, + /// Current time in audio samples + pub sample: SampleCount, + /// Current time in MIDI pulses + pub pulse: Pulse, +} + +/// +/// ``` +/// let _ = tek::Moment2::default(); +/// ``` +#[derive(Debug, Clone, Default)] pub enum Moment2 { + #[default] None, + Zero, + Usec(Microsecond), + Sample(SampleCount), + Pulse(Pulse), +} + +/// MIDI resolution in PPQ (pulses per quarter note) +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64); + +/// Timestamp in MIDI pulses +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64); + +/// Tempo in beats per minute +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64); + +/// Quantization setting for launching clips +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64); + +/// Quantization setting for notes +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64); + +/// Timestamp in audio samples +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64); + +/// Audio sample rate in Hz (samples per second) +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64); + +/// Timestamp in microseconds +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64); + +/// A unit of time, represented as an atomic 64-bit float. +/// +/// According to https://stackoverflow.com/a/873367, as per IEEE754, +/// every integer between 1 and 2^53 can be represented exactly. +/// This should mean that, even at 192kHz sampling rate, over 1 year of audio +/// can be clocked in microseconds with f64 without losing precision. +pub trait TimeUnit: InteriorMutable {} + +/// Contains memoized renders of clock values. +/// +/// Performance optimization. +#[derive(Debug)] pub struct ClockView { + pub sr: Memo, String>, + pub buf: Memo, String>, + pub lat: Memo, String>, + pub bpm: Memo, String>, + pub beat: Memo, String>, + pub time: Memo, String>, +} + +/// FIXME: remove this and use PPQ from timebase everywhere: +pub const PPQ: usize = 96; + +/// (pulses, name), assuming 96 PPQ +pub const NOTE_DURATIONS: [(usize, &str);26] = [ + (1, "1/384"), (2, "1/192"), + (3, "1/128"), (4, "1/96"), + (6, "1/64"), (8, "1/48"), + (12, "1/32"), (16, "1/24"), + (24, "1/16"), (32, "1/12"), + (48, "1/8"), (64, "1/6"), + (96, "1/4"), (128, "1/3"), + (192, "1/2"), (256, "2/3"), + (384, "1/1"), (512, "4/3"), + (576, "3/2"), (768, "2/1"), + (1152, "3/1"), (1536, "4/1"), + (2304, "6/1"), (3072, "8/1"), + (3456, "9/1"), (6144, "16/1"), +]; + +pub const NOTE_NAMES: [&str; 128] = [ + "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", + "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", + "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", + "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", + "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", + "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", + "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6", + "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", + "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", + "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", + "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", +]; + +pub const DEFAULT_PPQ: f64 = 96.0; + +def_command!(ClockCommand: |clock: Clock| { + SeekUsec { usec: f64 } => { + clock.playhead.update_from_usec(*usec); Ok(None) }, + SeekSample { sample: f64 } => { + clock.playhead.update_from_sample(*sample); Ok(None) }, + SeekPulse { pulse: f64 } => { + clock.playhead.update_from_pulse(*pulse); Ok(None) }, + SetBpm { bpm: f64 } => Ok(Some( + Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })), + SetQuant { quant: f64 } => Ok(Some( + Self::SetQuant { quant: clock.quant.set(*quant) })), + SetSync { sync: f64 } => Ok(Some( + Self::SetSync { sync: clock.sync.set(*sync) })), + + Play { position: Option } => { + clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ }, + Pause { position: Option } => { + clock.pause_at(*position)?; Ok(None) }, + + TogglePlayback { position: u32 } => Ok(if clock.is_rolling() { + clock.pause_at(Some(*position))?; None + } else { + clock.play_from(Some(*position))?; None + }), +}); + +impl Moment { + pub fn zero (timebase: &Arc) -> Self { + Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } + } + pub fn from_usec (timebase: &Arc, usec: f64) -> Self { + Self { + usec: usec.into(), + sample: timebase.sr.usecs_to_sample(usec).into(), + pulse: timebase.usecs_to_pulse(usec).into(), + timebase: timebase.clone(), + } + } + pub fn from_sample (timebase: &Arc, sample: f64) -> Self { + Self { + sample: sample.into(), + usec: timebase.sr.samples_to_usec(sample).into(), + pulse: timebase.samples_to_pulse(sample).into(), + timebase: timebase.clone(), + } + } + pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { + Self { + pulse: pulse.into(), + sample: timebase.pulses_to_sample(pulse).into(), + usec: timebase.pulses_to_usec(pulse).into(), + timebase: timebase.clone(), + } + } + #[inline] pub fn update_from_usec (&self, usec: f64) { + self.usec.set(usec); + self.pulse.set(self.timebase.usecs_to_pulse(usec)); + self.sample.set(self.timebase.sr.usecs_to_sample(usec)); + } + #[inline] pub fn update_from_sample (&self, sample: f64) { + self.usec.set(self.timebase.sr.samples_to_usec(sample)); + self.pulse.set(self.timebase.samples_to_pulse(sample)); + self.sample.set(sample); + } + #[inline] pub fn update_from_pulse (&self, pulse: f64) { + self.usec.set(self.timebase.pulses_to_usec(pulse)); + self.pulse.set(pulse); + self.sample.set(self.timebase.pulses_to_sample(pulse)); + } + #[inline] pub fn format_beat (&self) -> Arc { + self.timebase.format_beats_1(self.pulse.get()).into() + } +} +impl LaunchSync { + pub fn next (&self) -> f64 { + note_duration_next(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + note_duration_prev(self.get() as usize) as f64 + } +} +impl Quantize { + pub fn next (&self) -> f64 { + note_duration_next(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + note_duration_prev(self.get() as usize) as f64 + } +} +impl Timebase { + /// Specify sample rate, BPM and PPQ + pub fn new ( + s: impl Into, + b: impl Into, + p: impl Into + ) -> Self { + Self { sr: s.into(), bpm: b.into(), ppq: p.into() } + } + /// Iterate over ticks between start and end. + #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> Ticker { + Ticker { spp: self.samples_per_pulse(), sample: start, start, end } + } + /// Return the duration fo a beat in microseconds + #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } + /// Return the number of beats in a second + #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } + /// Return the number of microseconds corresponding to a note of the given duration + #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { + 4.0 * self.usec_per_beat() * num / den + } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } + /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) + #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } + /// Return number of pulses in a second (BPM-dependent) + #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } + /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) + #[inline] pub fn pulses_per_sample (&self) -> f64 { + self.usec_per_pulse() / self.sr.usec_per_sample() + } + /// Return number of samples in a pulse (SR- and BPM-dependent) + #[inline] pub fn samples_per_pulse (&self) -> f64 { + self.sr.get() / self.pulses_per_second() + } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { + self.pulses_per_sample() * p + } + /// Convert a number of samples to a pulse number (SR- and BPM-dependent) + #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { + s / self.pulses_per_sample() + } + /// Return the number of samples corresponding to a note of the given duration + #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { + self.usec_to_sample(self.note_to_usec(note)) + } + /// Return the number of samples corresponding to the given number of microseconds + #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { + usec * self.sr.get() / 1000f64 + } + /// Return the quantized position of a moment in time given a step + #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { + let step = self.note_to_usec(step); + (time / step, time % step) + } + /// Quantize a collection of events + #[inline] pub fn quantize_into + Sized, T> ( + &self, step: (f64, f64), events: E + ) -> Vec<(f64, f64)> { + events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 0 + #[inline] pub fn format_beats_0 (&self, pulse: f64) -> Arc { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + format!("{}.{}.{pulses:02}", beats / 4, beats % 4).into() + } + /// Format a number of pulses into Beat.Bar starting from 0 + #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4, beats % 4).into() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc { + let mut string = String::with_capacity(16); + self.format_beats_1_to(&mut string, pulse).expect("failed to format {pulse} into beat"); + string.into() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1_to (&self, w: &mut impl std::fmt::Write, pulse: f64) -> Result<(), std::fmt::Error> { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + write!(w, "{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4 + 1, beats % 4 + 1).into() + } +} +impl SampleRate { + /// Return the duration of a sample in microseconds (floating) + #[inline] pub fn usec_per_sample (&self) -> f64 { + 1_000_000f64 / self.get() + } + /// Return the duration of a sample in microseconds (floating) + #[inline] pub fn sample_per_usec (&self) -> f64 { + self.get() / 1_000_000f64 + } + /// Convert a number of samples to microseconds (floating) + #[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 { + self.usec_per_sample() * samples + } + /// Convert a number of microseconds to samples (floating) + #[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 { + self.sample_per_usec() * usecs + } +} +impl Microsecond { + #[inline] pub fn format_msu (&self) -> Arc { + let usecs = self.get() as usize; + let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); + let (minutes, seconds) = (seconds / 60, seconds % 60); + format!("{minutes}:{seconds:02}:{msecs:03}").into() + } +} + +/// Define and implement a unit of time +#[macro_export] macro_rules! impl_time_unit { +($T:ident) => { + impl Gettable for $T { + fn get (&self) -> f64 { self.0.load(Relaxed) } + } + impl InteriorMutable for $T { + fn set (&self, value: f64) -> f64 { + let old = self.get(); + self.0.store(value, Relaxed); + old + } + } + impl TimeUnit for $T {} + impl_op!($T, Add, add, |a, b|{a + b}); + impl_op!($T, Sub, sub, |a, b|{a - b}); + impl_op!($T, Mul, mul, |a, b|{a * b}); + impl_op!($T, Div, div, |a, b|{a / b}); + impl_op!($T, Rem, rem, |a, b|{a % b}); + impl From for $T { fn from (value: f64) -> Self { Self(value.into()) } } + impl From for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } } + impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } } + impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } } + impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } } + impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } } + impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } +} +} + +impl_time_unit!(SampleCount); +impl_time_unit!(SampleRate); +impl_time_unit!(Microsecond); +impl_time_unit!(Quantize); +impl_time_unit!(Ppq); +impl_time_unit!(Pulse); +impl_time_unit!(Bpm); +impl_time_unit!(LaunchSync); + impl std::fmt::Debug for Clock { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Clock") + .field("timebase", &self.timebase) + .field("chunk", &self.chunk) + .field("quant", &self.quant) + .field("sync", &self.sync) + .field("global", &self.global) + .field("playhead", &self.playhead) + .field("started", &self.started) + .finish() + } + } + impl Clock { + pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { + let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); + let timebase = Arc::new(Timebase::default()); + let clock = Self { + quant: Arc::new(24.into()), + sync: Arc::new(384.into()), + transport: Arc::new(Some(transport)), + chunk: Arc::new((chunk as usize).into()), + global: Arc::new(Moment::zero(&timebase)), + playhead: Arc::new(Moment::zero(&timebase)), + offset: Arc::new(Moment::zero(&timebase)), + started: RwLock::new(None).into(), + timebase, + midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), + midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), + click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), + ..Default::default() + }; + if let Some(bpm) = bpm { + clock.timebase.bpm.set(bpm); + } + Ok(clock) + } + pub fn timebase (&self) -> &Arc { + &self.timebase + } + /// Current sample rate + pub fn sr (&self) -> &SampleRate { + &self.timebase.sr + } + /// Current tempo + pub fn bpm (&self) -> &Bpm { + &self.timebase.bpm + } + /// Current MIDI resolution + pub fn ppq (&self) -> &Ppq { + &self.timebase.ppq + } + /// Next pulse that matches launch sync (for phrase switchover) + pub fn next_launch_pulse (&self) -> usize { + let sync = self.sync.get() as usize; + let pulse = self.playhead.pulse.get() as usize; + if pulse % sync == 0 { + pulse + } else { + (pulse / sync + 1) * sync + } + } + /// Start playing, optionally seeking to a given location beforehand + pub fn play_from (&self, start: Option) -> Usually<()> { + if let Some(transport) = self.transport.as_ref() { + if let Some(start) = start { + transport.locate(start)?; + } + transport.start()?; + } + Ok(()) + } + /// Pause, optionally seeking to a given location afterwards + pub fn pause_at (&self, pause: Option) -> Usually<()> { + if let Some(transport) = self.transport.as_ref() { + transport.stop()?; + if let Some(pause) = pause { + transport.locate(pause)?; + } + } + Ok(()) + } + /// Is currently paused? + pub fn is_stopped (&self) -> bool { + self.started.read().unwrap().is_none() + } + /// Is currently playing? + pub fn is_rolling (&self) -> bool { + self.started.read().unwrap().is_some() + } + /// Update chunk size + pub fn set_chunk (&self, n_frames: usize) { + self.chunk.store(n_frames, Relaxed); + } + pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { + // Store buffer length + self.set_chunk(scope.n_frames() as usize); + + // Store reported global frame and usec + let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; + self.global.sample.set(current_frames as f64); + self.global.usec.set(current_usecs as f64); + + let mut started = self.started.write().unwrap(); + + // If transport has just started or just stopped, + // update starting point: + if let Some(transport) = self.transport.as_ref() { + match (transport.query_state()?, started.as_ref()) { + (TransportState::Rolling, None) => { + let moment = Moment::zero(&self.timebase); + moment.sample.set(current_frames as f64); + moment.usec.set(current_usecs as f64); + *started = Some(moment); + }, + (TransportState::Stopped, Some(_)) => { + *started = None; + }, + _ => {} + }; + } + + self.playhead.update_from_sample(started.as_ref() + .map(|started|current_frames as f64 - started.sample.get()) + .unwrap_or(0.)); + + Ok(()) + } + + pub fn bbt (&self) -> PositionBBT { + let pulse = self.playhead.pulse.get() as i32; + let ppq = self.timebase.ppq.get() as i32; + let bpm = self.timebase.bpm.get(); + let bar = (pulse / ppq) / 4; + PositionBBT { + bar: 1 + bar, + beat: 1 + (pulse / ppq) % 4, + tick: (pulse % ppq), + bar_start_tick: (bar * 4 * ppq) as f64, + beat_type: 4., + beats_per_bar: 4., + beats_per_minute: bpm, + ticks_per_beat: ppq as f64 + } + } + + pub fn next_launch_instant (&self) -> Moment { + Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) + } + + /// Get index of first sample to populate. + /// + /// Greater than 0 means that the first pulse of the clip + /// falls somewhere in the middle of the chunk. + pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ + (scope.last_frame_time() as usize).saturating_sub( + started.sample.get() as usize + + self.started.read().unwrap().as_ref().unwrap().sample.get() as usize + ) + } + + // Get iterator that emits sample paired with pulse. + // + // * Sample: index into output buffer at which to write MIDI event + // * Pulse: index into clip from which to take the MIDI event + // + // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. + pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> Ticker { + self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) + } + } + impl Clock { + fn _todo_provide_u32 (&self) -> u32 { + todo!() + } + fn _todo_provide_opt_u32 (&self) -> Option { + todo!() + } + fn _todo_provide_f64 (&self) -> f64 { + todo!() + } + } + impl Command for ClockCommand { + fn execute (&self, state: &mut T) -> Perhaps { + self.execute(state.clock_mut()) // awesome + } + } + impl ClockView { + pub const BEAT_EMPTY: &'static str = "-.-.--"; + pub const TIME_EMPTY: &'static str = "-.---s"; + pub const BPM_EMPTY: &'static str = "---.---"; + pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { + let rate = clock.timebase.sr.get(); + let chunk = clock.chunk.load(Relaxed) as f64; + let lat = chunk / rate * 1000.; + let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); + let mut cache = cache.write().unwrap(); + cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); + cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); + cache.sr.update(Some((compact, rate)), |buf,_,_|{ + buf.clear(); + if compact { + write!(buf, "{:.1}kHz", rate / 1000.) + } else { + write!(buf, "{:.0}Hz", rate) + } + }); + if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { + let pulse = clock.timebase.usecs_to_pulse(now); + let time = now/1000000.; + let bpm = clock.timebase.bpm.get(); + cache.beat.update(Some(pulse), |buf, _, _|{ + buf.clear(); + clock.timebase.format_beats_1_to(buf, pulse) + }); + cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); + cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); + } else { + cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY)); + cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY)); + cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY)); + } + } + } + impl_default!(ClockView: { + let mut beat = String::with_capacity(16); + let _ = write!(beat, "{}", Self::BEAT_EMPTY); + let mut time = String::with_capacity(16); + let _ = write!(time, "{}", Self::TIME_EMPTY); + let mut bpm = String::with_capacity(16); + let _ = write!(bpm, "{}", Self::BPM_EMPTY); + Self { + beat: Memo::new(None, beat), + time: Memo::new(None, time), + bpm: Memo::new(None, bpm), + sr: Memo::new(None, String::with_capacity(16)), + buf: Memo::new(None, String::with_capacity(16)), + lat: Memo::new(None, String::with_capacity(16)), + } + }); diff --git a/src/connect.rs b/src/connect.rs deleted file mode 100644 index 97bae48b..00000000 --- a/src/connect.rs +++ /dev/null @@ -1,306 +0,0 @@ -use crate::*; - -/// Audio input port. -#[derive(Debug)] pub struct AudioInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// Audio output port. -#[derive(Debug)] pub struct AudioOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI input port. -#[derive(Debug)] pub struct MidiInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of currently held notes. - pub held: Arc>, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI output port. -#[derive(Debug)] pub struct MidiOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, - /// List of currently held notes. - pub held: Arc>, - /// Buffer - pub note_buffer: Vec, - /// Buffer - pub output_buffer: Vec>>, -} - -#[derive(Clone, Debug, PartialEq)] pub enum ConnectName { - /** Exact match */ - Exact(Arc), - /** Match regular expression */ - RegExp(Arc), -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { - One, - All -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { - Missing, - Disconnected, - Connected, - Mismatch, -} - -/// Port connection manager. -/// -/// ``` -/// let connect = tek::Connect::default(); -/// ``` -#[derive(Clone, Debug, Default)] pub struct Connect { - pub name: Option, - pub scope: Option, - pub status: Arc, Arc, ConnectStatus)>>>, - pub info: Arc, -} - -impl Connect { - pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) - -> Vec - { - let mut connections = vec![]; - for port in exact.iter() { connections.push(Self::exact(port)) } - for port in re.iter() { connections.push(Self::regexp(port)) } - for port in re_all.iter() { connections.push(Self::regexp_all(port)) } - connections - } - /// Connect to this exact port - pub fn exact (name: impl AsRef) -> Self { - let info = format!("=:{}", name.as_ref()).into(); - let name = Some(Exact(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp (name: impl AsRef) -> Self { - let info = format!("~:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp_all (name: impl AsRef) -> Self { - let info = format!("+:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn info (&self) -> Arc { - format!(" ({}) {} {}", { - let status = self.status.read().unwrap(); - let mut ok = 0; - for (_, _, state) in status.iter() { - if *state == Connected { - ok += 1 - } - } - format!("{ok}/{}", status.len()) - }, match self.scope { - None => "x", - Some(One) => " ", - Some(All) => "*", - }, match &self.name { - None => format!("x"), - Some(Exact(name)) => format!("= {name}"), - Some(RegExp(name)) => format!("~ {name}"), - }).into() - } -} - -impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl> RegisterPorts for J { - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiInput::new(self.jack(), name, connect) - } - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiOutput::new(self.jack(), name, connect) - } - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioInput::new(self.jack(), name, connect) - } - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioOutput::new(self.jack(), name, connect) - } -} - -/// May create new MIDI input ports. -pub trait AddMidiIn { - fn midi_in_add (&mut self) -> Usually<()>; -} - -/// May create new MIDI output ports. -pub trait AddMidiOut { - fn midi_out_add (&mut self) -> Usually<()>; -} - -pub trait RegisterPorts: HasJack<'static> { - /// Register a MIDI input port. - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register a MIDI output port. - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio input port. - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio output port. - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; -} - -pub trait JackPort: HasJack<'static> { - - type Port: PortSpec + Default; - - type Pair: PortSpec + Default; - - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized; - - fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { - jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) - .map_err(|e|e.into()) - } - - fn port_name (&self) -> &Arc; - - fn connections (&self) -> &[Connect]; - - fn port (&self) -> &Port; - - fn port_mut (&mut self) -> &mut Port; - - fn into_port (self) -> Port where Self: Sized; - - fn close (self) -> Usually<()> where Self: Sized { - let jack = self.jack().clone(); - Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) - } - - fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { - self.with_client(|c|c.ports(re_name, re_type, flags)) - } - - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - - fn port_by_name (&self, name: impl AsRef) -> Option> { - self.with_client(|c|c.port_by_name(name.as_ref())) - } - - fn connect_to_matching <'k> (&'k self) -> Usually<()> { - for connect in self.connections().iter() { - match &connect.name { - Some(Exact(name)) => { - *connect.status.write().unwrap() = self.connect_exact(name)?; - }, - Some(RegExp(re)) => { - *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; - }, - _ => {}, - }; - } - Ok(()) - } - - fn connect_exact <'k> (&'k self, name: &str) -> - Usually, Arc, ConnectStatus)>> - { - self.with_client(move|c|{ - let mut status = vec![]; - for port in c.ports(None, None, PortFlags::empty()).iter() { - if port.as_str() == &*name { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected { - break - } - } - } - } - Ok(status) - }) - } - - fn connect_regexp <'k> ( - &'k self, re: &str, scope: Option - ) -> Usually, Arc, ConnectStatus)>> { - self.with_client(move|c|{ - let mut status = vec![]; - let ports = c.ports(Some(&re), None, PortFlags::empty()); - for port in ports.iter() { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected && scope == Some(One) { - break - } - } - } - Ok(status) - }) - } - - /** Connect to a matching port by name. */ - fn connect_to_name (&self, name: impl AsRef) -> Usually { - self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { - self.connect_to_unowned(port) - } else { - Ok(Missing) - }) - } - - /** Connect to a matching port by reference. */ - fn connect_to_unowned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } - - /** Connect to an owned matching port by reference. */ - fn connect_to_owned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } - -} diff --git a/src/device.rs b/src/device.rs index e69de29b..bd44bf77 100644 --- a/src/device.rs +++ b/src/device.rs @@ -0,0 +1,583 @@ + +impl Device { + pub fn name (&self) -> &str { + match self { + Self::Sampler(sampler) => sampler.name.as_ref(), + _ => todo!(), + } + } + pub fn midi_ins (&self) -> &[MidiInput] { + match self { + //Self::Sampler(Sampler { midi_in, .. }) => &[midi_in], + _ => todo!() + } + } + pub fn midi_outs (&self) -> &[MidiOutput] { + match self { + Self::Sampler(_) => &[], + _ => todo!() + } + } + pub fn audio_ins (&self) -> &[AudioInput] { + match self { + Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(), + _ => todo!() + } + } + pub fn audio_outs (&self) -> &[AudioOutput] { + match self { + Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(), + _ => todo!() + } + } +} + +/// A device that can be plugged into the chain. +/// +/// ``` +/// let device = tek::Device::default(); +/// ``` +#[derive(Debug, Default)] pub enum Device { + #[default] + Bypass, + Mute, + #[cfg(feature = "sampler")] + Sampler(Sampler), + #[cfg(feature = "lv2")] // TODO + Lv2(Lv2), + #[cfg(feature = "vst2")] // TODO + Vst2, + #[cfg(feature = "vst3")] // TODO + Vst3, + #[cfg(feature = "clap")] // TODO + Clap, + #[cfg(feature = "sf2")] // TODO + Sf2, +} + +/// Some sort of wrapper? +pub struct DeviceAudio<'a>(pub &'a mut Device); + +/// Audio input port. +#[derive(Debug)] pub struct AudioInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +/// Audio output port. +#[derive(Debug)] pub struct AudioOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +/// MIDI input port. +#[derive(Debug)] pub struct MidiInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of currently held notes. + pub held: Arc>, + /// List of ports to connect to. + pub connections: Vec, +} + +/// MIDI output port. +#[derive(Debug)] pub struct MidiOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, + /// List of currently held notes. + pub held: Arc>, + /// Buffer + pub note_buffer: Vec, + /// Buffer + pub output_buffer: Vec>>, +} + +#[derive(Clone, Debug, PartialEq)] pub enum ConnectName { + /** Exact match */ + Exact(Arc), + /** Match regular expression */ + RegExp(Arc), +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { + One, + All +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { + Missing, + Disconnected, + Connected, + Mismatch, +} + +/// Port connection manager. +/// +/// ``` +/// let connect = tek::Connect::default(); +/// ``` +#[derive(Clone, Debug, Default)] pub struct Connect { + pub name: Option, + pub scope: Option, + pub status: Arc, Arc, ConnectStatus)>>>, + pub info: Arc, +} + +impl Connect { + pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) + -> Vec + { + let mut connections = vec![]; + for port in exact.iter() { connections.push(Self::exact(port)) } + for port in re.iter() { connections.push(Self::regexp(port)) } + for port in re_all.iter() { connections.push(Self::regexp_all(port)) } + connections + } + /// Connect to this exact port + pub fn exact (name: impl AsRef) -> Self { + let info = format!("=:{}", name.as_ref()).into(); + let name = Some(Exact(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp (name: impl AsRef) -> Self { + let info = format!("~:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp_all (name: impl AsRef) -> Self { + let info = format!("+:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn info (&self) -> Arc { + format!(" ({}) {} {}", { + let status = self.status.read().unwrap(); + let mut ok = 0; + for (_, _, state) in status.iter() { + if *state == Connected { + ok += 1 + } + } + format!("{ok}/{}", status.len()) + }, match self.scope { + None => "x", + Some(One) => " ", + Some(All) => "*", + }, match &self.name { + None => format!("x"), + Some(Exact(name)) => format!("= {name}"), + Some(RegExp(name)) => format!("~ {name}"), + }).into() + } +} + +impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl> RegisterPorts for J { + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiInput::new(self.jack(), name, connect) + } + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiOutput::new(self.jack(), name, connect) + } + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioInput::new(self.jack(), name, connect) + } + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioOutput::new(self.jack(), name, connect) + } +} + +/// May create new MIDI input ports. +pub trait AddMidiIn { + fn midi_in_add (&mut self) -> Usually<()>; +} + +/// May create new MIDI output ports. +pub trait AddMidiOut { + fn midi_out_add (&mut self) -> Usually<()>; +} + +pub trait RegisterPorts: HasJack<'static> { + /// Register a MIDI input port. + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register a MIDI output port. + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio input port. + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio output port. + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; +} + +pub trait JackPort: HasJack<'static> { + + type Port: PortSpec + Default; + + type Pair: PortSpec + Default; + + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized; + + fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { + jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) + .map_err(|e|e.into()) + } + + fn port_name (&self) -> &Arc; + + fn connections (&self) -> &[Connect]; + + fn port (&self) -> &Port; + + fn port_mut (&mut self) -> &mut Port; + + fn into_port (self) -> Port where Self: Sized; + + fn close (self) -> Usually<()> where Self: Sized { + let jack = self.jack().clone(); + Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) + } + + fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { + self.with_client(|c|c.ports(re_name, re_type, flags)) + } + + fn port_by_id (&self, id: u32) -> Option> { + self.with_client(|c|c.port_by_id(id)) + } + + fn port_by_name (&self, name: impl AsRef) -> Option> { + self.with_client(|c|c.port_by_name(name.as_ref())) + } + + fn connect_to_matching <'k> (&'k self) -> Usually<()> { + for connect in self.connections().iter() { + match &connect.name { + Some(Exact(name)) => { + *connect.status.write().unwrap() = self.connect_exact(name)?; + }, + Some(RegExp(re)) => { + *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; + }, + _ => {}, + }; + } + Ok(()) + } + + fn connect_exact <'k> (&'k self, name: &str) -> + Usually, Arc, ConnectStatus)>> + { + self.with_client(move|c|{ + let mut status = vec![]; + for port in c.ports(None, None, PortFlags::empty()).iter() { + if port.as_str() == &*name { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected { + break + } + } + } + } + Ok(status) + }) + } + + fn connect_regexp <'k> ( + &'k self, re: &str, scope: Option + ) -> Usually, Arc, ConnectStatus)>> { + self.with_client(move|c|{ + let mut status = vec![]; + let ports = c.ports(Some(&re), None, PortFlags::empty()); + for port in ports.iter() { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected && scope == Some(One) { + break + } + } + } + Ok(status) + }) + } + + /** Connect to a matching port by name. */ + fn connect_to_name (&self, name: impl AsRef) -> Usually { + self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { + self.connect_to_unowned(port) + } else { + Ok(Missing) + }) + } + + /** Connect to a matching port by reference. */ + fn connect_to_unowned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } + + /** Connect to an owned matching port by reference. */ + fn connect_to_owned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } +} + +impl JackPort for MidiInput { + type Port = MidiIn; + type Pair = MidiOut; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec(), + held: Arc::new(RwLock::new([false;128])) + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl JackPort for MidiOutput { + type Port = MidiOut; + type Pair = MidiIn; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self::register(jack, name)?; + let jack = jack.clone(); + let name = name.as_ref().into(); + let connections = connect.to_vec(); + let port = Self { + jack, + port, + name, + connections, + held: Arc::new([false;128].into()), + note_buffer: vec![0;8], + output_buffer: vec![vec![];65536], + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl MidiOutput { + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); + for frame in &mut self.output_buffer[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(&mut self.output_buffer); + } + } + /// Write a note to the output buffer + pub fn buffer_write <'a> ( + &'a mut self, + sample: usize, + event: LiveEvent, + ) { + self.note_buffer.fill(0); + event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); + self.output_buffer[sample].push(self.note_buffer.clone()); + // Update the list of currently held notes. + if let LiveEvent::Midi { ref message, .. } = event { + update_keys(&mut*self.held.write().unwrap(), message); + } + } + /// Write a chunk of MIDI data from the output buffer to the output port. + pub fn buffer_emit (&mut self, scope: &ProcessScope) { + let samples = scope.n_frames() as usize; + let mut writer = self.port.writer(scope); + for (time, events) in self.output_buffer.iter().enumerate().take(samples) { + for bytes in events.iter() { + writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ + panic!("Failed to write MIDI data: {bytes:?}"); + }); + } + } + } +} +impl MidiInput { + pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { + parse_midi_input(self.port().iter(scope)) + } +} +impl> + AsMut>> HasMidiIns for T { + fn midi_ins (&self) -> &Vec { self.as_ref() } + fn midi_ins_mut (&mut self) -> &mut Vec { self.as_mut() } +} +impl> + AsMut>> HasMidiOuts for T { + fn midi_outs (&self) -> &Vec { self.as_ref() } + fn midi_outs_mut (&mut self) -> &mut Vec { self.as_mut() } +} +impl> AddMidiIn for T { + fn midi_in_add (&mut self) -> Usually<()> { + let index = self.midi_ins().len(); + let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; + self.midi_ins_mut().push(port); + Ok(()) + } +} +/// Trail for thing that may gain new MIDI ports. +impl> AddMidiOut for T { + fn midi_out_add (&mut self) -> Usually<()> { + let index = self.midi_outs().len(); + let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; + self.midi_outs_mut().push(port); + Ok(()) + } +} + +impl JackPort for AudioInput { + type Port = AudioIn; + type Pair = AudioOut; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec() + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl JackPort for AudioOutput { + type Port = AudioOut; + type Pair = AudioIn; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec() + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl_audio!(|self: DeviceAudio<'a>, client, scope|{ + use Device::*; + match self.0 { + Mute => { Control::Continue }, + Bypass => { /*TODO*/ Control::Continue }, + #[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope), + #[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope), + #[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO + #[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO + #[cfg(feature = "clap")] Clap => { todo!() }, // TODO + #[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO + } +}); diff --git a/src/mix.rs b/src/mix.rs index 63248378..4e3a0ec2 100644 --- a/src/mix.rs +++ b/src/mix.rs @@ -12,3 +12,59 @@ Average, } + +#[cfg(test)] mod test_view_meter { + use super::*; + use proptest::prelude::*; + proptest! { + + #[test] fn proptest_view_meter ( + label in "\\PC*", value in f32::MIN..f32::MAX + ) { + let _ = view_meter(&label, value); + } + + #[test] fn proptest_view_meters ( + value1 in f32::MIN..f32::MAX, + value2 in f32::MIN..f32::MAX + ) { + let _ = view_meters(&[value1, value2]); + } + } +} + + impl Draw for RmsMeter { + fn draw(self, to: &mut Tui) -> Usually> { + let XYWH(x, y, w, h) = to.area(); + let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); + } + } + } + } + + impl Draw for Log10Meter { + fn draw(self, to: &mut Tui) -> Usually> { + let XYWH(x, y, w, h) = to.area(); + let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32 / 100.0).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2 - y, Some(Style::default().green())); + } + } + } + } + + fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { + Tui::bg(Black, w_exact(2, iter_east(1, ||meters.iter(), |value, _index|{ + h_full(RmsMeter(*value)) + }))) + } diff --git a/src/plugin.rs b/src/plugin.rs index 01d287f4..13d9aae8 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -26,3 +26,153 @@ pub window: Option } +impl_audio!(Lv2: lv2_jack_process); + +impl Lv2 { + const INPUT_BUFFER: usize = 1024; + pub fn new ( + jack: &Jack<'static>, + name: &str, + uri: &str, + ) -> Usually { + let lv2_world = livi::World::with_load_bundle(&uri); + let lv2_features = lv2_world.build_features(livi::FeaturesBuilder { + min_block_length: 1, + max_block_length: 65536, + }); + let lv2_plugin = lv2_world.iter_plugins().nth(0) + .unwrap_or_else(||panic!("plugin not found: {uri}")); + Ok(Self { + jack: jack.clone(), + name: name.into(), + path: Some(String::from(uri).into()), + selected: 0, + mapping: false, + midi_ins: vec![], + midi_outs: vec![], + audio_ins: vec![], + audio_outs: vec![], + lv2_instance: unsafe { + lv2_plugin + .instantiate(lv2_features.clone(), 48000.0) + .expect(&format!("instantiate failed: {uri}")) + }, + lv2_port_list: lv2_plugin.ports().collect::>(), + lv2_input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), + lv2_ui_thread: None, + lv2_world, + lv2_features, + lv2_plugin, + }) + } +} +fn lv2_jack_process ( + Lv2 { + midi_ins, midi_outs, audio_ins, audio_outs, + lv2_features, lv2_instance, lv2_input_buffer, .. + }: &mut Lv2, + _client: &Client, + scope: &ProcessScope +) -> Control { + let urid = lv2_features.midi_urid(); + lv2_input_buffer.clear(); + for port in midi_ins.iter() { + let mut atom = ::livi::event::LV2AtomSequence::new( + &lv2_features, + scope.n_frames() as usize + ); + for event in port.iter(scope) { + match event.bytes.len() { + 3 => atom.push_midi_event::<3>( + event.time as i64, + urid, + &event.bytes[0..3] + ).unwrap(), + _ => {} + } + } + lv2_input_buffer.push(atom); + } + let mut outputs = vec![]; + for _ in midi_outs.iter() { + outputs.push(::livi::event::LV2AtomSequence::new( + lv2_features, + scope.n_frames() as usize + )); + } + let ports = ::livi::EmptyPortConnections::new() + .with_atom_sequence_inputs(lv2_input_buffer.iter()) + .with_atom_sequence_outputs(outputs.iter_mut()) + .with_audio_inputs(audio_ins.iter().map(|o|o.as_slice(scope))) + .with_audio_outputs(audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); + unsafe { + lv2_instance.run(scope.n_frames() as usize, ports).unwrap() + }; + Control::Continue +} + +impl LV2PluginUI { pub fn new () -> Usually { Ok(Self { window: None }) } } + +impl ApplicationHandler for LV2PluginUI { + fn resumed (&mut self, event_loop: &ActiveEventLoop) { + self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); + } + fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { + match event { + WindowEvent::CloseRequested => { + self.window.as_ref().unwrap().set_visible(false); + event_loop.exit(); + }, + WindowEvent::RedrawRequested => { + self.window.as_ref().unwrap().request_redraw(); + } + _ => (), + } + } +} + +impl Draw for Lv2 { + fn draw(self, to: &mut Tui) { + let area = to.area(); + let XYWH(x, y, _, height) = area; + let mut width = 20u16; + let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1)); + let end = start + height as usize - 2; + //draw_box(buf, Rect { x, y, width, height }); + for i in start..end { + if let Some(port) = self.lv2_port_list.get(i) { + let value = if let Some(value) = self.lv2_instance.control_input(port.index) { + value + } else { + port.default_value + }; + //let label = &format!("C·· M·· {:25} = {value:.03}", port.name); + let label = &format!("{:25} = {value:.03}", port.name); + width = width.max(label.len() as u16 + 4); + let style = if i == self.selected { + Some(Style::default().green()) + } else { + None + } ; + to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style); + } else { + break + } + } + draw_header(self, to, x, y, width); + } +} + + +fn draw_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) { + let style = Style::default().gray(); + let label1 = format!(" {}", state.name); + to.blit(&label1, x + 1, y, Some(style.white().bold())); + if let Some(ref path) = state.path { + let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]); + to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim())); + } + //Ok(Rect { x, y, width: w, height: 1 }) +} + +#[cfg(feature = "vst2")] impl ::vst::host::Host for Plugin {} diff --git a/src/sample.rs b/src/sample.rs index 773644cc..74fba95c 100644 --- a/src/sample.rs +++ b/src/sample.rs @@ -1,3 +1,6 @@ +use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; +use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput}; + /// Plays [Voice]s from [Sample]s. /// /// ``` @@ -102,3 +105,549 @@ pub type MidiSample = (Option, Arc>); + +impl Default for SampleKit { + fn default () -> Self { Self([const { None }; N]) } +} +impl Iterator for Voice { + type Item = [f32;2]; + fn next (&mut self) -> Option { + if self.after > 0 { + self.after -= 1; + return Some([0.0, 0.0]) + } + let sample = self.sample.read().unwrap(); + if self.position < sample.end { + let position = self.position; + self.position += 1; + return sample.channels[0].get(position).map(|_amplitude|[ + sample.channels[0][position] * self.velocity * sample.gain, + sample.channels[0][position] * self.velocity * sample.gain, + ]) + } + None + } +} +impl NoteRange for Sampler { + fn note_lo (&self) -> &AtomicUsize { + &self.note_lo + } + fn note_axis (&self) -> &AtomicUsize { + &self.size.y + } +} +impl NotePoint for Sampler { + fn note_len (&self) -> &AtomicUsize { + unreachable!(); + } + fn get_note_len (&self) -> usize { + 0 + } + fn set_note_len (&self, _x: usize) -> usize { + 0 /*TODO?*/ + } + fn note_pos (&self) -> &AtomicUsize { + &self.note_pt + } + fn get_note_pos (&self) -> usize { + self.note_pt.load(Relaxed) + } + fn set_note_pos (&self, x: usize) -> usize { + let old = self.note_pt.swap(x, Relaxed); + self.cursor.0.store(x % 8, Relaxed); + self.cursor.1.store(x / 8, Relaxed); + old + } +} +impl Sampler { + pub fn new ( + jack: &Jack<'static>, + name: impl AsRef, + #[cfg(feature = "port")] midi_from: &[Connect], + #[cfg(feature = "port")] audio_from: &[&[Connect];2], + #[cfg(feature = "port")] audio_to: &[&[Connect];2], + ) -> Usually { + let name = name.as_ref(); + Ok(Self { + name: name.into(), + input_meters: vec![0.0;2], + output_meters: vec![0.0;2], + output_gain: 1., + buffer: vec![vec![0.0;16384];2], + #[cfg(feature = "port")] midi_in: Some( + MidiInput::new(jack, &format!("M/{name}"), midi_from)? + ), + #[cfg(feature = "port")] audio_ins: vec![ + AudioInput::new(jack, &format!("L/{name}"), audio_from[0])?, + AudioInput::new(jack, &format!("R/{name}"), audio_from[1])?, + ], + #[cfg(feature = "port")] audio_outs: vec![ + AudioOutput::new(jack, &format!("{name}/L"), audio_to[0])?, + AudioOutput::new(jack, &format!("{name}/R"), audio_to[1])?, + ], + ..Default::default() + }) + } + /// Value of cursor + pub fn cursor (&self) -> (usize, usize) { + (self.cursor.0.load(Relaxed), self.cursor.1.load(Relaxed)) + } + fn sample_selected (&self) -> usize { + (self.get_note_pos() as u8).into() + } + fn sample_selected_pitch (&self) -> u7 { + (self.get_note_pos() as u8).into() + } + pub fn process_audio_in (&mut self, scope: &ProcessScope) { + self.reset_input_meters(); + if self.recording.is_some() { + self.record_into(scope); + } else { + self.update_input_meters(scope); + } + } + /// Make sure that input meter count corresponds to input channel count + fn reset_input_meters (&mut self) { + let channels = self.audio_ins.len(); + if self.input_meters.len() != channels { + self.input_meters = vec![f32::MIN;channels]; + } + } + /// Record from inputs to sample + fn record_into (&mut self, scope: &ProcessScope) { + if let Some(ref sample) = self.recording.as_ref().expect("no recording sample").1 { + let mut sample = sample.write().unwrap(); + if sample.channels.len() != self.audio_ins.len() { + panic!("channel count mismatch"); + } + let samples_with_meters = self.audio_ins.iter() + .zip(self.input_meters.iter_mut()) + .zip(sample.channels.iter_mut()); + let mut length = 0; + for ((input, meter), channel) in samples_with_meters { + let slice = input.port().as_slice(scope); + length = length.max(slice.len()); + *meter = to_rms(slice); + channel.extend_from_slice(slice); + } + sample.end += length; + } else { + panic!("tried to record into the void") + } + } + /// Update input meters + fn update_input_meters (&mut self, scope: &ProcessScope) { + for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { + let slice = input.port().as_slice(scope); + *meter = to_rms(slice); + } + } + /// Make sure that output meter count corresponds to input channel count + fn reset_output_meters (&mut self) { + let channels = self.audio_outs.len(); + if self.output_meters.len() != channels { + self.output_meters = vec![f32::MIN;channels]; + } + } + /// Mix all currently playing samples into the output. + pub fn process_audio_out (&mut self, scope: &ProcessScope) { + self.clear_output_buffer(); + self.populate_output_buffer(scope.n_frames() as usize); + self.write_output_buffer(scope); + } + /// Zero the output buffer. + fn clear_output_buffer (&mut self) { + for buffer in self.buffer.iter_mut() { + buffer.fill(0.0); + } + } + /// Write playing voices to output buffer + fn populate_output_buffer (&mut self, frames: usize) { + let Sampler { buffer, voices, output_gain, mixing_mode, .. } = self; + let _channel_count = buffer.len(); + match mixing_mode { + MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{ + mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) + }), + MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{ + mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) + }), + } + } + /// Write output buffer to output ports. + fn write_output_buffer (&mut self, scope: &ProcessScope) { + let Sampler { audio_outs, buffer, .. } = self; + for (i, port) in audio_outs.iter_mut().enumerate() { + let buffer = &buffer[i]; + for (i, value) in port.port_mut().as_mut_slice(scope).iter_mut().enumerate() { + *value = *buffer.get(i).unwrap_or(&0.0); + } + } + } +} +impl SampleAdd { + fn exited (&self) -> bool { + self.exited + } + fn exit (&mut self) { + self.exited = true + } + pub fn new ( + sample: &Arc>, + voices: &Arc>> + ) -> Usually { + let dir = std::env::current_dir()?; + let (subdirs, files) = scan(&dir)?; + Ok(Self { + exited: false, + dir, + subdirs, + files, + cursor: 0, + offset: 0, + sample: sample.clone(), + voices: voices.clone(), + _search: None + }) + } + fn rescan (&mut self) -> Usually<()> { + scan(&self.dir).map(|(subdirs, files)|{ + self.subdirs = subdirs; + self.files = files; + }) + } + fn prev (&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + fn next (&mut self) { + self.cursor = self.cursor + 1; + } + fn try_preview (&mut self) -> Usually<()> { + if let Some(path) = self.cursor_file() { + if let Ok(sample) = Sample::from_file(&path) { + *self.sample.write().unwrap() = sample; + self.voices.write().unwrap().push( + Sample::play(&self.sample, 0, &u7::from(100u8)) + ); + } + //load_sample(&path)?; + //let src = std::fs::File::open(&path)?; + //let mss = MediaSourceStream::new(Box::new(src), Default::default()); + //let mut hint = Hint::new(); + //if let Some(ext) = path.extension() { + //hint.with_extension(&ext.to_string_lossy()); + //} + //let meta_opts: MetadataOptions = Default::default(); + //let fmt_opts: FormatOptions = Default::default(); + //if let Ok(mut probed) = symphonia::default::get_probe() + //.format(&hint, mss, &fmt_opts, &meta_opts) + //{ + //panic!("{:?}", probed.format.metadata()); + //}; + } + Ok(()) + } + fn cursor_dir (&self) -> Option { + if self.cursor < self.subdirs.len() { + Some(self.dir.join(&self.subdirs[self.cursor])) + } else { + None + } + } + fn cursor_file (&self) -> Option { + if self.cursor < self.subdirs.len() { + return None + } + let index = self.cursor.saturating_sub(self.subdirs.len()); + if index < self.files.len() { + Some(self.dir.join(&self.files[index])) + } else { + None + } + } + fn pick (&mut self) -> Usually { + if self.cursor == 0 { + if let Some(parent) = self.dir.parent() { + self.dir = parent.into(); + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + } + if let Some(dir) = self.cursor_dir() { + self.dir = dir; + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + if let Some(path) = self.cursor_file() { + let (end, channels) = read_sample_data(&path.to_string_lossy())?; + let mut sample = self.sample.write().unwrap(); + sample.name = path.file_name().unwrap().to_string_lossy().into(); + sample.end = end; + sample.channels = channels; + return Ok(true) + } + return Ok(false) + } +} +impl SampleKit { + pub fn get (&self, index: usize) -> &Option>> { + if index < self.0.len() { + &self.0[index] + } else { + &None + } + } +} +impl Sample { + pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { + Self { + name: name.as_ref().into(), + start, + end, + channels, + rate: None, + gain: 1.0, + color: ItemTheme::random(), + } + } + pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { + Voice { + sample: sample.clone(), + after, + position: sample.read().unwrap().start, + velocity: velocity.as_int() as f32 / 127.0, + } + } + pub fn handle_cc (&mut self, controller: u7, value: u7) { + let percentage = value.as_int() as f64 / 127.; + match controller.as_int() { + 20 => { + self.start = (percentage * self.end as f64) as usize; + }, + 21 => { + let length = self.channels[0].len(); + self.end = length.min( + self.start + (percentage * (length as f64 - self.start as f64)) as usize + ); + }, + 22 => { /*attack*/ }, + 23 => { /*decay*/ }, + 24 => { + self.gain = percentage as f32 * 2.0; + }, + 26 => { /* pan */ } + 25 => { /* pitch */ } + _ => {} + } + } + /// Read WAV from file + pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { + let mut channels: Vec> = vec![]; + for channel in wavers::Wav::from_path(src)?.channels() { + channels.push(channel); + } + let mut end = 0; + let mut data: Vec> = vec![]; + for samples in channels.iter() { + let channel = Vec::from(samples.as_ref()); + end = end.max(channel.len()); + data.push(channel); + } + Ok((end, data)) + } + pub fn from_file (path: &PathBuf) -> Usually { + let name = path.file_name().unwrap().to_string_lossy().into(); + let mut sample = Self { name, ..Default::default() }; + // Use file extension if present + let mut hint = Hint::new(); + if let Some(ext) = path.extension() { + hint.with_extension(&ext.to_string_lossy()); + } + let probed = symphonia::default::get_probe().format( + &hint, + MediaSourceStream::new( + Box::new(File::open(path)?), + Default::default(), + ), + &Default::default(), + &Default::default() + )?; + let mut format = probed.format; + let params = &format.tracks().iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .expect("no tracks found") + .codec_params; + let mut decoder = get_codecs().make(params, &Default::default())?; + loop { + match format.next_packet() { + Ok(packet) => sample.decode_packet(&mut decoder, packet)?, + Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), + Err(err) => return Err(err.into()), + }; + }; + sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); + Ok(sample) + } + fn decode_packet ( + &mut self, decoder: &mut Box, packet: Packet + ) -> Usually<()> { + // Decode a packet + let decoded = decoder + .decode(&packet) + .map_err(|e|Box::::from(e))?; + // Determine sample rate + let spec = *decoded.spec(); + if let Some(rate) = self.rate { + if rate != spec.rate as usize { + panic!("sample rate changed"); + } + } else { + self.rate = Some(spec.rate as usize); + } + // Determine channel count + while self.channels.len() < spec.channels.count() { + self.channels.push(vec![]); + } + // Load sample + let mut samples = SampleBuffer::new( + decoded.frames() as u64, + spec + ); + if samples.capacity() > 0 { + samples.copy_interleaved_ref(decoded); + for frame in samples.samples().chunks(spec.channels.count()) { + for (chan, frame) in frame.iter().enumerate() { + self.channels[chan].push(*frame) + } + } + } + Ok(()) + } +} +impl Draw for SampleAdd { + fn draw (self, _to: &mut Tui) -> Usually> { + todo!() + } +} + +fn draw_list_item (sample: &Option>>) -> String { + if let Some(sample) = sample { + let sample = sample.read().unwrap(); + format!("{:8}", sample.name) + //format!("{:8} {:3} {:6}-{:6}/{:6}", + //sample.name, + //sample.gain, + //sample.start, + //sample.end, + //sample.channels[0].len() + //) + } else { + String::from("........") + } +} + +fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + use<'_> { + let min_db = -64.0; + Thunk::new(move|to: &mut Tui|{ + let XYWH(x, y, width, height) = to.area(); + let area = Rect { x, y, width, height }; + if let Some(sample) = &sample { + let sample = sample.read().unwrap(); + let start = sample.start as f64; + let end = sample.end as f64; + let length = end - start; + let step = length / width as f64; + let mut t = start; + let mut lines = vec![]; + while t < end { + let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; + let total: f32 = chunk.iter().map(|x|x.abs()).sum(); + let count = chunk.len() as f32; + let meter = 10. * (total / count).log10(); + let x = t as f64; + let y = meter as f64; + lines.push(Line::new(x, min_db, x, y, Color::Green)); + t += step / 2.; + } + Canvas::default() + .x_bounds([sample.start as f64, sample.end as f64]) + .y_bounds([min_db, 0.]) + .paint(|ctx| { + for line in lines.iter() { + ctx.draw(line); + } + //FIXME: proportions + //let text = "press record to finish sampling"; + //ctx.print( + //(width - text.len() as u16) as f64 / 2.0, + //height as f64 / 2.0, + //text.red() + //); + }).render(area, &mut to.buffer); + } else { + Canvas::default() + .x_bounds([0.0, width as f64]) + .y_bounds([0.0, height as f64]) + .paint(|_ctx| { + //let text = "press record to begin sampling"; + //ctx.print( + //(width - text.len() as u16) as f64 / 2.0, + //height as f64 / 2.0, + //text.red() + //); + }) + .render(area, &mut to.buffer); + } + }) +} + +impl_audio!(Sampler: sampler_jack_process); +pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control { + if let Some(midi_in) = &state.midi_in { + for midi in midi_in.port().iter(scope) { + sampler_midi_in(&state.samples, &state.voices, midi) + } + } + state.process_audio_out(scope); + state.process_audio_in(scope); + Control::Continue +} + +/// Create [Voice]s from [Sample]s in response to MIDI input. +fn sampler_midi_in ( + samples: &SampleKit<128>, voices: &Arc>>, RawMidi { time, bytes }: RawMidi +) { + if let Ok(LiveEvent::Midi { message, .. }) = LiveEvent::parse(bytes) { + match message { + MidiMessage::NoteOn { ref key, ref vel } => { + if let Some(sample) = samples.get(key.as_int() as usize) { + voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); + } + }, + MidiMessage::Controller { controller: _, value: _ } => { + // TODO + } + _ => {} + } + } +} + +fn draw_sample ( + to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool +) -> Usually { + let style = if focus { Style::default().green() } else { Style::default() }; + if focus { + to.blit(&"🬴", x+1, y, Some(style.bold())); + } + let label1 = format!("{:3} {:12}", + note.map(|n|n.to_string()).unwrap_or(String::default()), + sample.name); + let label2 = format!("{:>6} {:>6} +0.0", + sample.start, + sample.end); + to.blit(&label1, x+2, y, Some(style.bold())); + to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); + Ok(label1.len() + label2.len() + 4) +} diff --git a/src/sequence.rs b/src/sequence.rs index 049b6dae..1e55f0dc 100644 --- a/src/sequence.rs +++ b/src/sequence.rs @@ -1,3 +1,4 @@ +use ::std::sync::{Arc, RwLock}; /// Contains state for viewing and editing a clip. /// @@ -594,3 +595,822 @@ pub trait MidiRange: TimeRange + NoteRange {} // Lowest note displayed pub note_lo: Arc, } + +impl MidiClip { + pub fn new ( + name: impl AsRef, + looped: bool, + length: usize, + notes: Option, + color: Option, + ) -> Self { + Self { + uuid: uuid::Uuid::new_v4(), + name: name.as_ref().into(), + ppq: PPQ, + length, + notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), + looped, + loop_start: 0, + loop_length: length, + percussive: true, + color: color.unwrap_or_else(ItemTheme::random) + } + } + pub fn count_midi_messages (&self) -> usize { + let mut count = 0; + for tick in self.notes.iter() { + count += tick.len(); + } + count + } + pub fn set_length (&mut self, length: usize) { + self.length = length; + self.notes = vec![Vec::with_capacity(16);length]; + } + pub fn duplicate (&self) -> Self { + let mut clone = self.clone(); + clone.uuid = uuid::Uuid::new_v4(); + clone + } + pub fn toggle_loop (&mut self) { self.looped = !self.looped; } + pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { + if pulse >= self.length { panic!("extend clip first") } + self.notes[pulse].push(message); + } + /// Check if a range `start..end` contains MIDI Note On `k` + pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { + for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { + for event in events.iter() { + if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } + } + } + false + } + pub fn stop_all () -> Self { + Self::new( + "Stop", + false, + 1, + Some(vec![vec![MidiMessage::Controller { + controller: 123.into(), + value: 0.into() + }]]), + Some(ItemColor::from_tui(Color::Rgb(32, 32, 32)).into()) + ) + } +} + +impl PartialEq for MidiClip { + fn eq (&self, other: &Self) -> bool { + self.uuid == other.uuid + } +} + +impl Eq for MidiClip {} + +impl MidiClip { + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_bool_stub_ (&self) -> bool { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } +} +impl_has!(Sequencer: |self: Track| self.sequencer); +impl_has!(Clock: |self: Sequencer| self.clock); +impl_has!(Vec: |self: Sequencer| self.midi_ins); +impl_has!(Vec: |self: Sequencer| self.midi_outs); +impl_has!(Measure: |self: MidiEditor| self.size); +impl_has!(Measure: |self: PianoHorizontal| self.size); +impl_default!(Sequencer: Self { + clock: Clock::default(), + play_clip: None, + next_clip: None, + midi_ins: vec![], + midi_outs: vec![], + recording: false, + monitoring: true, + overdub: false, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + note_buf: vec![0;8], + midi_buf: vec![], + reset: true, +}); +impl Sequencer { + pub fn new ( + name: impl AsRef, + jack: &Jack<'static>, + #[cfg(feature = "clock")] clock: Option<&Clock>, + #[cfg(feature = "clip")] clip: Option<&Arc>>, + #[cfg(feature = "port")] midi_from: &[Connect], + #[cfg(feature = "port")] midi_to: &[Connect], + ) -> Usually { + let _name = name.as_ref(); + #[cfg(feature = "clock")] let clock = clock.cloned().unwrap_or_default(); + Ok(Self { + reset: true, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + #[cfg(feature = "port")] midi_ins: vec![MidiInput::new(jack, &format!("M/{}", name.as_ref()), midi_from)?,], + #[cfg(feature = "port")] midi_outs: vec![MidiOutput::new(jack, &format!("{}/M", name.as_ref()), midi_to)?, ], + #[cfg(feature = "clip")] play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), + #[cfg(feature = "clock")] clock, + ..Default::default() + }) + } + fn process_rolling (&mut self, scope: &ProcessScope) -> Control { + self.process_clear(scope, false); + // Write chunk of clip to output, handle switchover + if self.process_playback(scope) { + self.process_switchover(scope); + } + // Monitor input to output + self.process_monitoring(scope); + // Record and/or monitor input + self.process_recording(scope); + // Emit contents of MIDI buffers to JACK MIDI output ports. + self.midi_outs_emit(scope); + Control::Continue + } + fn process_stopped (&mut self, scope: &ProcessScope) -> Control { + if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { + self.process_monitoring(scope) + } + Control::Continue + } + fn process_monitoring (&mut self, scope: &ProcessScope) { + let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat + let monitoring = self.monitoring(); + for input in self.midi_ins.iter() { + for (sample, event, bytes) in input.parsed(scope) { + if let LiveEvent::Midi { message, .. } = event { + if monitoring { + self.midi_buf[sample].push(bytes.to_vec()); + } + // FIXME: don't lock on every event! + update_keys(&mut notes_in.write().unwrap(), &message); + } + } + } + } + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); + for frame in &mut self.midi_buf_mut()[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(self.midi_buf_mut()); + } + for port in self.midi_outs_mut().iter_mut() { + // Clear output buffer(s) + port.buffer_clear(scope, false); + } + } + fn process_recording (&mut self, scope: &ProcessScope) { + if self.monitoring() { + self.monitor(scope); + } + if let Some((started, ref clip)) = self.play_clip.clone() { + self.record_clip(scope, started, clip); + } + if let Some((_start_at, _clip)) = &self.next_clip() { + self.record_next(); + } + } + fn process_playback (&mut self, scope: &ProcessScope) -> bool { + // If a clip is playing, write a chunk of MIDI events from it to the output buffer. + // If no clip is playing, prepare for switchover immediately. + if let Some((started, clip)) = &self.play_clip { + // Length of clip, to repeat or stop on end. + let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); + // Index of first sample to populate. + let offset = self.clock().get_sample_offset(scope, &started); + // Write MIDI events from clip at sample offsets corresponding to pulses. + for (sample, pulse) in self.clock().get_pulses(scope, offset) { + // If a next clip is enqueued, and we're past the end of the current one, + // break the loop here (FIXME count pulse correctly) + let past_end = if clip.is_some() { pulse >= length } else { true }; + // Is it time for switchover? + if self.next_clip().is_some() && past_end { + return true + } + // If there's a currently playing clip, output notes from it to buffer: + if let Some(clip) = clip { + // Source clip from which the MIDI events will be taken. + let clip = clip.read().unwrap(); + // Clip with zero length is not processed + if clip.length > 0 { + // Current pulse index in source clip + let pulse = pulse % clip.length; + // Output each MIDI event from clip at appropriate frames of output buffer: + for message in clip.notes[pulse].iter() { + for port in self.midi_outs.iter_mut() { + port.buffer_write(sample, LiveEvent::Midi { + channel: 0.into(), /* TODO */ + message: *message + }); + } + } + } + } + } + false + } else { + true + } + } + /// Handle switchover from current to next playing clip. + fn process_switchover (&mut self, scope: &ProcessScope) { + let _midi_buf = self.midi_buf_mut(); + let sample0 = scope.last_frame_time() as usize; + //let samples = scope.n_frames() as usize; + if let Some((start_at, clip)) = &self.next_clip() { + let start = start_at.sample.get() as usize; + let sample = self.clock().started.read().unwrap() + .as_ref().unwrap().sample.get() as usize; + // If it's time to switch to the next clip: + if start <= sample0.saturating_sub(sample) { + // Samples elapsed since clip was supposed to start + let _skipped = sample0 - start; + // Switch over to enqueued clip + let started = Moment::from_sample(self.clock().timebase(), start as f64); + // Launch enqueued clip + *self.play_clip_mut() = Some((started, clip.clone())); + // Unset enqueuement (TODO: where to implement looping?) + *self.next_clip_mut() = None; + // Fill in remaining ticks of chunk from next clip. + self.process_playback(scope); + } + } + } +} +impl HasMidiBuffers for Sequencer { + fn note_buf_mut (&mut self) -> &mut Vec { &mut self.note_buf } + fn midi_buf_mut (&mut self) -> &mut Vec>> { &mut self.midi_buf } +} +impl std::fmt::Debug for Sequencer { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Sequencer") + .field("clock", &self.clock) + .field("play_clip", &self.play_clip) + .field("next_clip", &self.next_clip) + .finish() + } +} +impl MidiMonitor for Sequencer { + fn monitoring (&self) -> bool { self.monitoring } + fn monitoring_mut (&mut self) -> &mut bool { &mut self.monitoring } + fn notes_in (&self) -> &Arc> { &self.notes_in } +} +impl MidiRecord for Sequencer { + fn recording (&self) -> bool { self.recording } + fn recording_mut (&mut self) -> &mut bool { &mut self.recording } + fn overdub (&self) -> bool { self.overdub } + fn overdub_mut (&mut self) -> &mut bool { &mut self.overdub } +} +#[cfg(feature="clip")] impl HasPlayClip for Sequencer { + fn reset (&self) -> bool { self.reset } + fn reset_mut (&mut self) -> &mut bool { &mut self.reset } + fn play_clip (&self) -> &Option<(Moment, Option>>)> { + &self.play_clip + } + fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.play_clip + } + fn next_clip (&self) -> &Option<(Moment, Option>>)> { + &self.next_clip + } + fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.next_clip + } +} +/// JACK process callback for a sequencer's clip sequencer/recorder. +impl Audio for Sequencer { + fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + if self.clock().is_rolling() { + self.process_rolling(scope) + } else { + self.process_stopped(scope) + } + } +} +impl Draw for MidiEditor { + fn draw(self, to: &mut Tui) -> Usually> { + self.tui().draw(to) + } +} +impl Draw for PianoHorizontal { + fn draw(self, to: &mut Tui) -> Usually> { + self.tui().draw(to) + } +} +impl std::fmt::Debug for MidiEditor { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("MidiEditor").field("mode", &self.mode).finish() + } +} +impl_from!(MidiEditor: |clip: &Arc>| { + let model = Self::from(Some(clip.clone())); + model.redraw(); + model +}); +impl_from!(MidiEditor: |clip: Option>>| { + let mut model = Self::default(); + *model.clip_mut() = clip; + model.redraw(); + model +}); +impl_default!(MidiEditor: Self { + size: Measure::new(0, 0), mode: PianoHorizontal::new(None) +}); +impl_default!(OctaveVertical: Self { + on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] +}); +impl MidiEditor { + /// Put note at current position + pub fn put_note (&mut self, advance: bool) { + let mut redraw = false; + if let Some(clip) = self.clip() { + let mut clip = clip.write().unwrap(); + let note_start = self.get_time_pos(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let note_end = note_start + (note_len.saturating_sub(1)); + let key: u7 = u7::from(note_pos as u8); + let vel: u7 = 100.into(); + let length = clip.length; + let note_end = note_end % length; + let note_on = MidiMessage::NoteOn { key, vel }; + if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { + clip.notes[note_start].push(note_on); + } + let note_off = MidiMessage::NoteOff { key, vel }; + if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { + clip.notes[note_end].push(note_off); + } + if advance { + self.set_time_pos((note_end + 1) % clip.length); + } + redraw = true; + } + if redraw { + self.mode.redraw(); + } + } + fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } + fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } + fn note_length (&self) -> usize { self.get_note_len() } + fn note_pos (&self) -> usize { self.get_note_pos() } + fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } + fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } + fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } + fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } + fn note_len (&self) -> usize { self.get_note_len() } + fn note_len_next (&self) -> usize { self.get_note_len() + 1 } + fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } + fn note_range (&self) -> usize { self.get_note_axis() } + fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } + fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } + fn time_zoom (&self) -> usize { self.get_time_zoom() } + fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_lock (&self) -> bool { self.get_time_lock() } + fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } + fn time_pos (&self) -> usize { self.get_time_pos() } + fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } + fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } + fn time_pos_prev (&self) -> usize { + let step = self.get_note_len(); + self.get_time_pos().overflowing_sub(step) + .0.min(self.clip_length().saturating_sub(step)) + } + fn time_pos_prev_fine (&self) -> usize { + self.get_time_pos().overflowing_sub(1) + .0.min(self.clip_length().saturating_sub(1)) + } + pub fn clip_status (&self) -> impl Draw + '_ { + let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.name.clone(), clip.length, clip.looped) + } else { (ItemTheme::G[64], String::new().into(), 0, false) }; + w_exact(20, south!( + w_full(origin_w(east( + button_2("f2", "name ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), + w_full(origin_w(east( + button_2("l", "ength ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), + w_full(origin_w(east( + button_2("r", "epeat ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), + )) + } + pub fn edit_status (&self) -> impl Draw + '_ { + let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.length) + } else { (ItemTheme::G[64], 0) }; + let time_pos = self.get_time_pos(); + let time_zoom = self.get_time_zoom(); + let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; + let note_pos = self.get_note_pos(); + let note_name = format!("{:4}", note_pitch_to_name(note_pos)); + let note_pos = format!("{:>3}", note_pos); + let note_len = format!("{:>4}", self.get_note_len()); + w_exact(20, south!( + w_full(origin_w(east( + button_2("t", "ime ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), + format!("{length} /{time_zoom} +{time_pos} "))))))), + w_full(origin_w(east( + button_2("z", "lock ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), + format!("{time_lock}"))))))), + w_full(origin_w(east( + button_2("x", "note ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), + format!("{note_name} {note_pos} {note_len}"))))))), + )) + } +} + +impl TimeRange for MidiEditor { + fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } +} + +impl NoteRange for MidiEditor { + fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } +} + +impl NotePoint for MidiEditor { + fn note_len (&self) -> &AtomicUsize { self.mode.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } +} + +impl TimePoint for MidiEditor { + fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } +} + +impl MidiViewer for MidiEditor { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } + fn redraw (&self) { self.mode.redraw() } + fn clip (&self) -> &Option>> { self.mode.clip() } + fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } + fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } +} + +impl MidiEditor { + fn tui (&self) -> impl Draw { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } +} + + +impl PianoHorizontal { + pub fn new (clip: Option<&Arc>>) -> Self { + let size = Measure::new(0, 0); + let mut range = MidiSelection::from((12, true)); + range.time_axis = size.x.clone(); + range.note_axis = size.y.clone(); + let piano = Self { + keys_width: 5, + size, + range, + buffer: RwLock::new(Default::default()).into(), + point: MidiCursor::default(), + clip: clip.cloned(), + color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), + }; + piano.redraw(); + piano + } +} + +impl PianoHorizontal { + fn tui (&self) -> impl Draw { + south( + east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), + east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(self.cursor())))), + ) + } +} + +impl PianoHorizontal { + /// Draw the piano roll background. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { + for (y, note) in (0..=127).rev().enumerate() { + for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { + let cell = buf.get_mut(x, y).unwrap(); + if note == (127-note_point) || time == time_point { + cell.set_bg(Rgb(0,0,0)); + } else { + cell.set_bg(clip.color.darkest.rgb); + } + if time % 384 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('│'); + } else if time % 96 == 0 { + cell.set_fg(clip.color.dark.rgb); + cell.set_char('╎'); + } else if time % note_len == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('┊'); + } else if (127 - note) % 12 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('='); + } else if (127 - note) % 6 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('—'); + } else { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('·'); + } + } + } + } + /// Draw the piano roll foreground. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { + let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); + let mut notes_on = [false;128]; + for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { + for (_y, note) in (0..=127).rev().enumerate() { + if let Some(cell) = buf.get_mut(x, note) { + if notes_on[note] { + cell.set_char('▂'); + cell.set_style(style); + } + } + } + let time_end = time_start + zoom; + for time in time_start..time_end.min(clip.length) { + for event in clip.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + if let Some(cell) = buf.get_mut(x, note) { + cell.set_char('█'); + cell.set_style(style); + } + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} + } + } + } + + } + } + fn notes (&self) -> impl Draw { + let time_start = self.get_time_start(); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + let buffer = self.buffer.clone(); + Thunk::new(move|to: &mut Tui|{ + let source = buffer.read().unwrap(); + let XYWH(x0, y0, w, _h) = to.area(); + //if h as usize != note_axis { + //panic!("area height mismatch: {h} <> {note_axis}"); + //} + for (area_x, screen_x) in (x0..x0+w).enumerate() { + for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { + let source_x = time_start + area_x; + let source_y = note_hi - area_y; + // TODO: enable loop rollover: + //let source_x = (time_start + area_x) % source.width.max(1); + //let source_y = (note_hi - area_y) % source.height.max(1); + let is_in_x = source_x < source.width; + let is_in_y = source_y < source.height; + if is_in_x && is_in_y { + if let Some(source_cell) = source.get(source_x, source_y) { + if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { + *cell = source_cell.clone(); + } + } + } + } + } + }) + } + fn cursor (&self) -> impl Draw { + let note_hi = self.get_note_hi(); + let note_lo = self.get_note_lo(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let time_pos = self.get_time_pos(); + let time_start = self.get_time_start(); + let time_zoom = self.get_time_zoom(); + let style = Some(Style::default().fg(self.color.lightest.rgb)); + Thunk::new(move|to: &mut Tui|{ + let XYWH(x0, y0, w, _) = to.area(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + if note == note_pos { + for x in 0..w { + let screen_x = x0 + x; + let time_1 = time_start + x as usize * time_zoom; + let time_2 = time_1 + time_zoom; + if time_1 <= time_pos && time_pos < time_2 { + to.blit(&"█", screen_x, screen_y, style); + let tail = note_len as u16 / time_zoom as u16; + for x_tail in (screen_x + 1)..(screen_x + tail) { + to.blit(&"▂", x_tail, screen_y, style); + } + break + } + } + break + } + } + }) + } + fn keys (&self) -> impl Draw { + let state = self; + let color = state.color; + let note_lo = state.get_note_lo(); + let note_hi = state.get_note_hi(); + let note_pos = state.get_note_pos(); + let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); + let off_style = Some(Style::default().fg(Tui::g(255))); + let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); + h_full(w_exact(self.keys_width, Thunk::new(move|to: &mut Tui|{ + let XYWH(x, y0, _w, _h) = to.area(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + to.blit(&to_key(note), x, screen_y, key_style); + if note > 127 { + continue + } + if note == note_pos { + to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style) + } else { + to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) + }; + } + }))) + } + fn timeline (&self) -> impl Draw + '_ { + w_full(h_exact(1, Thunk::new(move|to: &mut Tui|{ + let XYWH(x, y, w, _h) = to.area(); + let style = Some(Style::default().dim()); + let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { + let t = area_x as usize * self.time_zoom().get(); + if t < length { + to.blit(&"|", screen_x, y, style); + } + } + }))) + } +} + +impl TimeRange for PianoHorizontal { + fn time_len (&self) -> &AtomicUsize { self.range.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.range.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } +} + +impl NoteRange for PianoHorizontal { + fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } +} + +impl NotePoint for PianoHorizontal { + fn note_len (&self) -> &AtomicUsize { self.point.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } +} + +impl TimePoint for PianoHorizontal { + fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } +} + +impl MidiViewer for PianoHorizontal { + fn clip (&self) -> &Option>> { &self.clip } + fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } + /// Determine the required space to render the clip. + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { (clip.length / self.range.time_zoom().get(), 128) } + fn redraw(self) { + *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { + let clip = clip.read().unwrap(); + let buf_size = self.buffer_size(&clip); + let mut buffer = BigBuffer::from(buf_size); + let time_zoom = self.get_time_zoom(); + self.time_len().set(clip.length); + PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); + PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); + buffer + } else { + Default::default() + } + } + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); + self.redraw(); + } +} + +impl std::fmt::Debug for PianoHorizontal { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + let buffer = self.buffer.read().unwrap(); + f.debug_struct("PianoHorizontal") + .field("time_zoom", &self.range.time_zoom) + .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) + .finish() + } +} +impl OctaveVertical { + fn color (&self, pitch: usize) -> Color { + let pitch = pitch % 12; + self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] + } +} +impl OctaveVertical { + fn tui (&self) -> impl Draw { + east!( + Tui::fg_bg(self.color(0), self.color(1), "▙"), + Tui::fg_bg(self.color(2), self.color(3), "▙"), + Tui::fg_bg(self.color(4), self.color(5), "▌"), + Tui::fg_bg(self.color(6), self.color(7), "▟"), + Tui::fg_bg(self.color(8), self.color(9), "▟"), + Tui::fg_bg(self.color(10), self.color(11), "▟"), + ) + } +} +impl_from!(MidiSelection: |data:(usize, bool)| Self { + time_len: Arc::new(0.into()), + note_axis: Arc::new(0.into()), + note_lo: Arc::new(0.into()), + time_axis: Arc::new(0.into()), + time_start: Arc::new(0.into()), + time_zoom: Arc::new(data.0.into()), + time_lock: Arc::new(data.1.into()), +}); +impl_default!(MidiCursor: Self { + time_pos: Arc::new(0.into()), + note_pos: Arc::new(36.into()), + note_len: Arc::new(24.into()), +}); + +impl NotePoint for MidiCursor { + fn note_len (&self) -> &AtomicUsize { + &self.note_len + } + fn note_pos (&self) -> &AtomicUsize { + &self.note_pos + } +} + +impl TimePoint for MidiCursor { + fn time_pos (&self) -> &AtomicUsize { + self.time_pos.as_ref() + } +} + +impl TimeRange for MidiSelection { + fn time_len (&self) -> &AtomicUsize { &self.time_len } + fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } + fn time_lock (&self) -> &AtomicBool { &self.time_lock } + fn time_start (&self) -> &AtomicUsize { &self.time_start } + fn time_axis (&self) -> &AtomicUsize { &self.time_axis } +} + +impl NoteRange for MidiSelection { + fn note_lo (&self) -> &AtomicUsize { &self.note_lo } + fn note_axis (&self) -> &AtomicUsize { &self.note_axis } +} + +impl Iterator for Ticker { + type Item = (usize, usize); + fn next (&mut self) -> Option { + loop { + if self.sample > self.end { return None } + let spp = self.spp; + let sample = self.sample as f64; + let start = self.start; + let end = self.end; + self.sample += 1; + //println!("{spp} {sample} {start} {end}"); + let jitter = sample.rem_euclid(spp); // ramps + let next_jitter = (sample + 1.0).rem_euclid(spp); + if jitter > next_jitter { // at crossing: + let time = (sample as usize) % (end as usize-start as usize); + let tick = (sample / spp) as usize; + return Some((time, tick)) + } + } + } +} diff --git a/src/tek.rs b/src/tek.rs index dc2c3169..8e04a885 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -4,15 +4,48 @@ impl_trait_in_assoc_type, trait_alias, type_alias_impl_trait, type_changing_struct_update )] +/// Implement an arithmetic operation for a unit of time +#[macro_export] macro_rules! impl_op { + ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { + let $a = self.get(); let $b = other.get(); Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { + let $a = self.get(); let $b = other as f64; Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { + let $a = self.get(); let $b = other; Self($impl.into()) + } + } + } +} + +#[macro_export] macro_rules! impl_has_clips { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { + fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { + $cb.read().unwrap() + } + fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { + $cb.write().unwrap() + } + } + } +} + pub mod arrange; pub mod browse; -pub mod connect; +pub mod clock; pub mod device; pub mod mix; pub mod plugin; pub mod sample; pub mod sequence; -pub mod tick; use clap::{self, Parser, Subcommand}; use builder_pattern::Builder; @@ -74,10 +107,6 @@ pub(crate) use tengri::{ }, }; -pub(crate) use ConnectName::*; -pub(crate) use ConnectScope::*; -pub(crate) use ConnectStatus::*; - /// Command-line entrypoint. #[cfg(feature = "cli")] pub fn main () -> Usually<()> { Config::watch(|config|{ @@ -428,39 +457,6 @@ impl> + AsMut>> HasDevices for T { } } -impl Device { - pub fn name (&self) -> &str { - match self { - Self::Sampler(sampler) => sampler.name.as_ref(), - _ => todo!(), - } - } - pub fn midi_ins (&self) -> &[MidiInput] { - match self { - //Self::Sampler(Sampler { midi_in, .. }) => &[midi_in], - _ => todo!() - } - } - pub fn midi_outs (&self) -> &[MidiOutput] { - match self { - Self::Sampler(_) => &[], - _ => todo!() - } - } - pub fn audio_ins (&self) -> &[AudioInput] { - match self { - Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(), - _ => todo!() - } - } - pub fn audio_outs (&self) -> &[AudioOutput] { - match self { - Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(), - _ => todo!() - } - } -} - //take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); @@ -604,32 +600,6 @@ def_command!(AppCommand: |app: App| { }, }); -def_command!(ClockCommand: |clock: Clock| { - SeekUsec { usec: f64 } => { - clock.playhead.update_from_usec(*usec); Ok(None) }, - SeekSample { sample: f64 } => { - clock.playhead.update_from_sample(*sample); Ok(None) }, - SeekPulse { pulse: f64 } => { - clock.playhead.update_from_pulse(*pulse); Ok(None) }, - SetBpm { bpm: f64 } => Ok(Some( - Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })), - SetQuant { quant: f64 } => Ok(Some( - Self::SetQuant { quant: clock.quant.set(*quant) })), - SetSync { sync: f64 } => Ok(Some( - Self::SetSync { sync: clock.sync.set(*sync) })), - - Play { position: Option } => { - clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ }, - Pause { position: Option } => { - clock.pause_at(*position)?; Ok(None) }, - - TogglePlayback { position: u32 } => Ok(if clock.is_rolling() { - clock.pause_at(Some(*position))?; None - } else { - clock.play_from(Some(*position))?; None - }), -}); - def_command!(DeviceCommand: |device: Device| {}); def_command!(ClipCommand: |clip: MidiClip| { @@ -941,282 +911,217 @@ def_command!(TrackCommand: |track: Track| { toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }), }); -pub(crate) use self::size::*; -mod size { - use crate::*; - /// Define a type alias for iterators of sized items (columns). - macro_rules! def_sizes_iter { - ($Type:ident => $($Item:ty),+) => { - pub trait $Type<'a> = - Iterator + Send + Sync + 'a; - } +/// Define a type alias for iterators of sized items (columns). +macro_rules! def_sizes_iter { + ($Type:ident => $($Item:ty),+) => { + pub trait $Type<'a> = + Iterator + Send + Sync + 'a; } - def_sizes_iter!(InputsSizes => MidiInput); - def_sizes_iter!(OutputsSizes => MidiOutput); - def_sizes_iter!(PortsSizes => Arc, [Connect]); - def_sizes_iter!(ScenesSizes => Scene); - def_sizes_iter!(TracksSizes => Track); +} +def_sizes_iter!(InputsSizes => MidiInput); +def_sizes_iter!(OutputsSizes => MidiOutput); +def_sizes_iter!(PortsSizes => Arc, [Connect]); +def_sizes_iter!(ScenesSizes => Scene); +def_sizes_iter!(TracksSizes => Track); + +/// ``` +/// let _ = tek::view_logo(); +/// ``` +pub fn view_logo () -> impl Draw { + wh_exact(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), south!{ + h_exact(1, ""), + h_exact(1, ""), + h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), + h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), + h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), + }))) } -pub use self::view::*; -mod view { - use crate::*; - - /// ``` - /// let _ = tek::view_logo(); - /// ``` - pub fn view_logo () -> impl Draw { - wh_exact(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), south!{ - h_exact(1, ""), - h_exact(1, ""), - h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), - h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), - h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), - }))) - } - - /// ``` - /// let x = std::sync::Arc::>::default(); - /// let _ = tek::view_transport(true, x.clone(), x.clone(), x.clone()); - /// let _ = tek::view_transport(false, x.clone(), x.clone(), x.clone()); - /// ``` - pub fn view_transport ( - play: bool, - bpm: Arc>, - beat: Arc>, - time: Arc>, - ) -> impl Draw { - let theme = ItemTheme::G[96]; - Tui::bg(Black, east!(above( - wh_full(origin_w(button_play_pause(play))), - wh_full(origin_e(east!( - field_h(theme, "BPM", bpm), - field_h(theme, "Beat", beat), - field_h(theme, "Time", time), - ))) +/// ``` +/// let x = std::sync::Arc::>::default(); +/// let _ = tek::view_transport(true, x.clone(), x.clone(), x.clone()); +/// let _ = tek::view_transport(false, x.clone(), x.clone(), x.clone()); +/// ``` +pub fn view_transport ( + play: bool, + bpm: Arc>, + beat: Arc>, + time: Arc>, +) -> impl Draw { + let theme = ItemTheme::G[96]; + Tui::bg(Black, east!(above( + wh_full(origin_w(button_play_pause(play))), + wh_full(origin_e(east!( + field_h(theme, "BPM", bpm), + field_h(theme, "Beat", beat), + field_h(theme, "Time", time), ))) - } + ))) +} - /// ``` - /// let x = std::sync::Arc::>::default(); - /// let _ = tek::view_status(None, x.clone(), x.clone(), x.clone()); - /// let _ = tek::view_status(Some("".into()), x.clone(), x.clone(), x.clone()); - /// ``` - pub fn view_status ( - sel: Option>, - sr: Arc>, - buf: Arc>, - lat: Arc>, - ) -> impl Draw { - let theme = ItemTheme::G[96]; - Tui::bg(Black, east!(above( - wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))), - wh_full(origin_e(east!( - field_h(theme, "SR", sr), - field_h(theme, "Buf", buf), - field_h(theme, "Lat", lat), - ))) +/// ``` +/// let x = std::sync::Arc::>::default(); +/// let _ = tek::view_status(None, x.clone(), x.clone(), x.clone()); +/// let _ = tek::view_status(Some("".into()), x.clone(), x.clone(), x.clone()); +/// ``` +pub fn view_status ( + sel: Option>, + sr: Arc>, + buf: Arc>, + lat: Arc>, +) -> impl Draw { + let theme = ItemTheme::G[96]; + Tui::bg(Black, east!(above( + wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))), + wh_full(origin_e(east!( + field_h(theme, "SR", sr), + field_h(theme, "Buf", buf), + field_h(theme, "Lat", lat), ))) - } + ))) +} - /// ``` - /// let _ = tek::button_play_pause(true); - /// ``` - pub fn button_play_pause (playing: bool) -> impl Draw { - let compact = true;//self.is_editing(); - Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - either(compact, - Thunk::new(move|to: &mut Tui|to.place(&w_exact(9, either(playing, - Tui::fg(Rgb(0, 255, 0), " PLAYING "), - Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - )), - Thunk::new(move|to: &mut Tui|to.place(&w_exact(5, either(playing, - Tui::fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - Tui::fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)))) - )) - ) - ) - } - - #[cfg(feature = "track")] pub fn view_track_row_section ( - _theme: ItemTheme, - button: impl Draw, - button_add: impl Draw, - content: impl Draw, - ) -> impl Draw { - west(h_full(w_exact(4, origin_nw(button_add))), - east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content)))) - } - - /// ``` - /// let bg = tengri::ratatui::style::Color::Red; - /// let fg = tengri::ratatui::style::Color::Green; - /// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); - /// ``` - pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw { - let left = Tui::fg_bg(bg, Reset, w_exact(1, y_repeat("▐"))); - let right = Tui::fg_bg(bg, Reset, w_exact(1, y_repeat("▌"))); - east(left, west(right, Tui::fg_bg(fg, bg, content))) - } - - /// ``` - /// let _ = tek::view_meter("", 0.0); - /// let _ = tek::view_meters(&[0.0, 0.0]); - /// ``` - pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { - south!( - field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)), - wh_exact(if value >= 0.0 { 13 } - else if value >= -1.0 { 12 } - else if value >= -2.0 { 11 } - else if value >= -3.0 { 10 } - else if value >= -4.0 { 9 } - else if value >= -6.0 { 8 } - else if value >= -9.0 { 7 } - else if value >= -12.0 { 6 } - else if value >= -15.0 { 5 } - else if value >= -20.0 { 4 } - else if value >= -25.0 { 3 } - else if value >= -30.0 { 2 } - else if value >= -40.0 { 1 } - else { 0 }, 1, Tui::bg(if value >= 0.0 { Red } - else if value >= -3.0 { Yellow } - else { Green }, ()))) - } - - pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { - let left = format!("L/{:>+9.3}", values[0]); - let right = format!("R/{:>+9.3}", values[1]); - south(left, right) - } - - pub fn draw_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { - when(sample.is_some(), Thunk::new(move|to: &mut Tui|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - to.place(&east!( - field_h(theme, "Name", format!("{:<10}", sample.name.clone())), - field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), - field_h(theme, "Start", format!("{:<8}", sample.start)), - field_h(theme, "End", format!("{:<8}", sample.end)), - field_h(theme, "Trans", "0"), - field_h(theme, "Gain", format!("{}", sample.gain)), +/// ``` +/// let _ = tek::button_play_pause(true); +/// ``` +pub fn button_play_pause (playing: bool) -> impl Draw { + let compact = true;//self.is_editing(); + Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + either(compact, + Thunk::new(move|to: &mut Tui|to.place(&w_exact(9, either(playing, + Tui::fg(Rgb(0, 255, 0), " PLAYING "), + Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + )), + Thunk::new(move|to: &mut Tui|to.place(&w_exact(5, either(playing, + Tui::fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + Tui::fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)))) )) - })) - } - - pub fn draw_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { - either(sample.is_some(), Thunk::new(move|to: &mut Tui|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - to.place(&w_exact(20, south!( - w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))), - w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))), - w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))), - w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))), - w_full(origin_w(field_h(theme, "Trans ", "0"))), - w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))), - ))) - }), Thunk::new(|to: &mut Tui|to.place(&Tui::fg(Red, south!( - Tui::bold(true, "× No sample."), - "[r] record", - "[Shift-F9] import", - ))))) - } - - pub fn draw_status (sample: Option<&Arc>>) -> impl Draw { - Tui::bold(true, Tui::fg(Tui::g(224), sample - .map(|sample|{ - let sample = sample.read().unwrap(); - format!("Sample {}-{}", sample.start, sample.end) - }) - .unwrap_or_else(||"No sample".to_string()))) - } - - pub fn view_track_header (theme: ItemTheme, content: impl Draw) -> impl Draw { - w_exact(12, Tui::bg(theme.darker.rgb, w_full(origin_e(content)))) - } - - pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) - -> impl Draw + use<'a, T> - { - let ins = ports.len() as u16; - let frame = Outer(true, Style::default().fg(Tui::g(96))); - let iter = move||ports.iter(); - let names = iter_south(1, iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); - let field = field_v(theme, title, names); - wh_exact(20, 1 + ins, frame.enclose(wh_exact(20, 1 + ins, field))) - } - - pub fn io_ports <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a - ) -> impl Draw + 'a { - iter(items, move|( - _index, name, connections, y, y2 - ): (usize, &'a Arc, &'a [Connect], usize, usize), _| - iter_south(y as u16, (y2-y) as u16, south( - h_full(Tui::bold(true, Tui::fg_bg(fg, bg, origin_w(east(&" 󰣲 ", name))))), - iter(||connections.iter(), move|connect: &'a Connect, index|iter_south(index as u16, 1, - h_full(origin_w(Tui::bold(false, Tui::fg_bg(fg, bg, - &connect.info))))))))) - } + ) + ) } -#[cfg(test)] mod test_view_meter { - use super::*; - use proptest::prelude::*; - proptest! { - - #[test] fn proptest_view_meter ( - label in "\\PC*", value in f32::MIN..f32::MAX - ) { - let _ = view_meter(&label, value); - } - - #[test] fn proptest_view_meters ( - value1 in f32::MIN..f32::MAX, - value2 in f32::MIN..f32::MAX - ) { - let _ = view_meters(&[value1, value2]); - } - } +#[cfg(feature = "track")] pub fn view_track_row_section ( + _theme: ItemTheme, + button: impl Draw, + button_add: impl Draw, + content: impl Draw, +) -> impl Draw { + west(h_full(w_exact(4, origin_nw(button_add))), + east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content)))) } -pub const DEFAULT_PPQ: f64 = 96.0; +/// ``` +/// let bg = tengri::ratatui::style::Color::Red; +/// let fg = tengri::ratatui::style::Color::Green; +/// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); +/// ``` +pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw { + let left = Tui::fg_bg(bg, Reset, w_exact(1, y_repeat("▐"))); + let right = Tui::fg_bg(bg, Reset, w_exact(1, y_repeat("▌"))); + east(left, west(right, Tui::fg_bg(fg, bg, content))) +} -/// FIXME: remove this and use PPQ from timebase everywhere: -pub const PPQ: usize = 96; +/// ``` +/// let _ = tek::view_meter("", 0.0); +/// let _ = tek::view_meters(&[0.0, 0.0]); +/// ``` +pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { + south!( + field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)), + wh_exact(if value >= 0.0 { 13 } + else if value >= -1.0 { 12 } + else if value >= -2.0 { 11 } + else if value >= -3.0 { 10 } + else if value >= -4.0 { 9 } + else if value >= -6.0 { 8 } + else if value >= -9.0 { 7 } + else if value >= -12.0 { 6 } + else if value >= -15.0 { 5 } + else if value >= -20.0 { 4 } + else if value >= -25.0 { 3 } + else if value >= -30.0 { 2 } + else if value >= -40.0 { 1 } + else { 0 }, 1, Tui::bg(if value >= 0.0 { Red } + else if value >= -3.0 { Yellow } + else { Green }, ()))) +} -/// (pulses, name), assuming 96 PPQ -pub const NOTE_DURATIONS: [(usize, &str);26] = [ - (1, "1/384"), (2, "1/192"), - (3, "1/128"), (4, "1/96"), - (6, "1/64"), (8, "1/48"), - (12, "1/32"), (16, "1/24"), - (24, "1/16"), (32, "1/12"), - (48, "1/8"), (64, "1/6"), - (96, "1/4"), (128, "1/3"), - (192, "1/2"), (256, "2/3"), - (384, "1/1"), (512, "4/3"), - (576, "3/2"), (768, "2/1"), - (1152, "3/1"), (1536, "4/1"), - (2304, "6/1"), (3072, "8/1"), - (3456, "9/1"), (6144, "16/1"), -]; +pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { + let left = format!("L/{:>+9.3}", values[0]); + let right = format!("R/{:>+9.3}", values[1]); + south(left, right) +} -pub const NOTE_NAMES: [&str; 128] = [ - "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", - "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", - "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", - "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", - "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", - "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", - "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6", - "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", - "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", - "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", - "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", -]; +pub fn draw_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { + when(sample.is_some(), Thunk::new(move|to: &mut Tui|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + to.place(&east!( + field_h(theme, "Name", format!("{:<10}", sample.name.clone())), + field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), + field_h(theme, "Start", format!("{:<8}", sample.start)), + field_h(theme, "End", format!("{:<8}", sample.end)), + field_h(theme, "Trans", "0"), + field_h(theme, "Gain", format!("{}", sample.gain)), + )) + })) +} + +pub fn draw_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { + either(sample.is_some(), Thunk::new(move|to: &mut Tui|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + to.place(&w_exact(20, south!( + w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))), + w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))), + w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))), + w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))), + w_full(origin_w(field_h(theme, "Trans ", "0"))), + w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))), + ))) + }), Thunk::new(|to: &mut Tui|to.place(&Tui::fg(Red, south!( + Tui::bold(true, "× No sample."), + "[r] record", + "[Shift-F9] import", + ))))) +} + +pub fn draw_status (sample: Option<&Arc>>) -> impl Draw { + Tui::bold(true, Tui::fg(Tui::g(224), sample + .map(|sample|{ + let sample = sample.read().unwrap(); + format!("Sample {}-{}", sample.start, sample.end) + }) + .unwrap_or_else(||"No sample".to_string()))) +} + +pub fn view_track_header (theme: ItemTheme, content: impl Draw) -> impl Draw { + w_exact(12, Tui::bg(theme.darker.rgb, w_full(origin_e(content)))) +} + +pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) + -> impl Draw + use<'a, T> +{ + let ins = ports.len() as u16; + let frame = Outer(true, Style::default().fg(Tui::g(96))); + let iter = move||ports.iter(); + let names = iter_south(1, iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); + let field = field_v(theme, title, names); + wh_exact(20, 1 + ins, frame.enclose(wh_exact(20, 1 + ins, field))) +} + +pub fn io_ports <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a +) -> impl Draw + 'a { + iter(items, move|( + _index, name, connections, y, y2 + ): (usize, &'a Arc, &'a [Connect], usize, usize), _| + iter_south(y as u16, (y2-y) as u16, south( + h_full(Tui::bold(true, Tui::fg_bg(fg, bg, origin_w(east(&" 󰣲 ", name))))), + iter(||connections.iter(), move|connect: &'a Connect, index|iter_south(index as u16, 1, + h_full(origin_w(Tui::bold(false, Tui::fg_bg(fg, bg, + &connect.info))))))))) +} /// CLI banner. pub(crate) const HEADER: &'static str = r#" @@ -1501,308 +1406,776 @@ pub trait HasWidth { Options, } -/// A device that can be plugged into the chain. -/// -/// ``` -/// let device = tek::Device::default(); -/// ``` -#[derive(Debug, Default)] pub enum Device { - #[default] - Bypass, - Mute, - #[cfg(feature = "sampler")] - Sampler(Sampler), - #[cfg(feature = "lv2")] // TODO - Lv2(Lv2), - #[cfg(feature = "vst2")] // TODO - Vst2, - #[cfg(feature = "vst3")] // TODO - Vst3, - #[cfg(feature = "clap")] // TODO - Clap, - #[cfg(feature = "sf2")] // TODO - Sf2, +/// Command-line configuration. +#[cfg(feature = "cli")] impl Cli { + pub fn run (&self) -> Usually<()> { + if let Action::Version = self.action { + return Ok(tek_show_version()) + } + + let mut config = Config::new(None); + config.init()?; + + if let Action::Config = self.action { + tek_print_config(&config); + } else if let Action::List = self.action { + todo!("list sessions") + } else if let Action::Resume = self.action { + todo!("resume session") + } else if let Action::New { + name, bpm, tracks, scenes, sync_lead, sync_follow, + midi_from, midi_from_re, midi_to, midi_to_re, + left_from, right_from, left_to, right_to, .. + } = &self.action { + + // Connect to JACK + let name = name.as_ref().map_or("tek", |x|x.as_str()); + let jack = Jack::new(&name)?; + + // TODO: Collect audio IO: + let empty = &[] as &[&str]; + let left_froms = Connect::collect(&left_from, empty, empty); + let left_tos = Connect::collect(&left_to, empty, empty); + let right_froms = Connect::collect(&right_from, empty, empty); + let right_tos = Connect::collect(&right_to, empty, empty); + let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; + let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; + + // Create initial project: + let clock = Clock::new(&jack, *bpm)?; + let mut project = Arrangement::new( + &jack, + None, + clock, + vec![], + vec![], + Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() + .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) + .collect::, _>>()?, + Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() + .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) + .collect::, _>>()? + ); + project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; + project.scenes_add(scenes.unwrap_or(0))?; + + if matches!(self.action, Action::Status) { + // Show status and exit + tek_print_status(&project); + return Ok(()) + } + + // Initialize the app state + let app = tek(&jack, project, config, ":menu"); + if matches!(self.action, Action::Headless) { + // TODO: Headless mode (daemon + client over IPC, then over network...) + println!("todo headless"); + return Ok(()) + } + + // Run the [Tui] and [Jack] threads with the [App] state. + Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ + + // Between jack init and app's first cycle: + + jack.sync_lead(*sync_lead, |mut state|{ + let clock = app.clock(); + clock.playhead.update_from_sample(state.position.frame() as f64); + state.position.bbt = Some(clock.bbt()); + state.position + })?; + + jack.sync_follow(*sync_follow)?; + + // FIXME: They don't work properly. + + Ok(app) + + })?)?; + } + Ok(()) + } } -/// Some sort of wrapper? -pub struct DeviceAudio<'a>(pub &'a mut Device); - - /// Command-line configuration. - #[cfg(feature = "cli")] impl Cli { - pub fn run (&self) -> Usually<()> { - if let Action::Version = self.action { - return Ok(tek_show_version()) - } - - let mut config = Config::new(None); - config.init()?; - - if let Action::Config = self.action { - tek_print_config(&config); - } else if let Action::List = self.action { - todo!("list sessions") - } else if let Action::Resume = self.action { - todo!("resume session") - } else if let Action::New { - name, bpm, tracks, scenes, sync_lead, sync_follow, - midi_from, midi_from_re, midi_to, midi_to_re, - left_from, right_from, left_to, right_to, .. - } = &self.action { - - // Connect to JACK - let name = name.as_ref().map_or("tek", |x|x.as_str()); - let jack = Jack::new(&name)?; - - // TODO: Collect audio IO: - let empty = &[] as &[&str]; - let left_froms = Connect::collect(&left_from, empty, empty); - let left_tos = Connect::collect(&left_to, empty, empty); - let right_froms = Connect::collect(&right_from, empty, empty); - let right_tos = Connect::collect(&right_to, empty, empty); - let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; - let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; - - // Create initial project: - let clock = Clock::new(&jack, *bpm)?; - let mut project = Arrangement::new( - &jack, - None, - clock, - vec![], - vec![], - Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() - .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) - .collect::, _>>()?, - Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() - .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) - .collect::, _>>()? - ); - project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; - project.scenes_add(scenes.unwrap_or(0))?; - - if matches!(self.action, Action::Status) { - // Show status and exit - tek_print_status(&project); - return Ok(()) - } - - // Initialize the app state - let app = tek(&jack, project, config, ":menu"); - if matches!(self.action, Action::Headless) { - // TODO: Headless mode (daemon + client over IPC, then over network...) - println!("todo headless"); - return Ok(()) - } - - // Run the [Tui] and [Jack] threads with the [App] state. - Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ - - // Between jack init and app's first cycle: - - jack.sync_lead(*sync_lead, |mut state|{ - let clock = app.clock(); - clock.playhead.update_from_sample(state.position.frame() as f64); - state.position.bbt = Some(clock.bbt()); - state.position - })?; - - jack.sync_follow(*sync_follow)?; - - // FIXME: They don't work properly. - - Ok(app) - - })?)?; +impl Config { + const CONFIG_DIR: &'static str = "tek"; + const CONFIG_SUB: &'static str = "v0"; + const CONFIG: &'static str = "tek.edn"; + const DEFAULTS: &'static str = include_str!("./tek.edn"); + /// Create a new app configuration from a set of XDG base directories, + pub fn new (dirs: Option) -> Self { + let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); + let dirs = dirs.unwrap_or_else(default); + Self { dirs, ..Default::default() } + } + /// Write initial contents of configuration. + pub fn init (&mut self) -> Usually<()> { + self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ + cfgs.add(&dsl)?; + Ok(()) + })?; + Ok(()) + } + /// Write initial contents of a configuration file. + pub fn init_one ( + &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> + ) -> Usually<()> { + if self.dirs.find_config_file(path).is_none() { + //println!("Creating {path:?}"); + std::fs::write(self.dirs.place_config_file(path)?, defaults)?; + } + Ok(if let Some(path) = self.dirs.find_config_file(path) { + //println!("Loading {path:?}"); + let src = std::fs::read_to_string(&path)?; + src.as_str().each(move|item|each(self, item))?; + } else { + return Err(format!("{path}: not found").into()) + }) + } + /// Add statements to configuration from [Dsl] source. + pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { + dsl.each(|item|self.add_one(item))?; + Ok(self) + } + fn add_one (&self, item: impl Language) -> Usually<()> { + if let Some(expr) = item.expr()? { + let head = expr.head()?; + let tail = expr.tail()?; + let name = tail.head()?; + let body = tail.tail()?; + //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); + match head { + Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, + Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, + Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, + _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) } Ok(()) + } else { + return Err(format!("Config::load: expected expr, got: {item:?}").into()) } } + pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { + self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() + } +} - impl Config { - const CONFIG_DIR: &'static str = "tek"; - const CONFIG_SUB: &'static str = "v0"; - const CONFIG: &'static str = "tek.edn"; - const DEFAULTS: &'static str = include_str!("./tek.edn"); - /// Create a new app configuration from a set of XDG base directories, - pub fn new (dirs: Option) -> Self { - let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); - let dirs = dirs.unwrap_or_else(default); - Self { dirs, ..Default::default() } +impl Mode> { + /// Add a definition to the mode. + /// + /// Supported definitions: + /// + /// - (name ...) -> name + /// - (info ...) -> description + /// - (keys ...) -> key bindings + /// - (mode ...) -> submode + /// - ... -> view + /// + /// ``` + /// let mut mode: tek::Mode> = Default::default(); + /// mode.add("(name hello)").unwrap(); + /// ``` + pub fn add (&mut self, dsl: impl Language) -> Usually<()> { + Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { + //println!("Mode::add: {head} {:?}", expr.tail()); + let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + match head { + "name" => self.add_name(tail)?, + "info" => self.add_info(tail)?, + "keys" => self.add_keys(tail)?, + "mode" => self.add_mode(tail)?, + _ => self.add_view(tail)?, + }; + } else if let Ok(Some(word)) = dsl.word() { + self.add_view(word); + } else { + return Err(format!("Mode::add: unexpected: {dsl:?}").into()); + }) + + //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) + //.word(|word|self.add_view(word)) + //.expr(|expr|expr.head(|head|{ + ////println!("Mode::add: {head} {:?}", expr.tail()); + //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + //match head { + //"name" => self.add_name(tail), + //"info" => self.add_info(tail), + //"keys" => self.add_keys(tail)?, + //"mode" => self.add_mode(tail)?, + //_ => self.add_view(tail), + //}; + //})) + } + + fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.name.push(src.into()))) + } + fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.info.push(src.into()))) + } + fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.view.push(src.into()))) + } + fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) + } + fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(if let Some(id) = dsl.head()? { + load_mode(&self.modes, &id, &dsl.tail())?; + } else { + return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); + })) + } +} + +impl Bind { + /// Create a new event map + pub fn new () -> Self { + Default::default() + } + /// Add a binding to an owned event map. + pub fn def (mut self, event: E, binding: Binding) -> Self { + self.add(event, binding); + self + } + /// Add a binding to an event map. + pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { + if !self.0.contains_key(&event) { + self.0.insert(event.clone(), Default::default()); } - /// Write initial contents of configuration. - pub fn init (&mut self) -> Usually<()> { - self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ - cfgs.add(&dsl)?; - Ok(()) - })?; + self.0.get_mut(&event).unwrap().push(binding); + self + } + /// Return the binding(s) that correspond to an event. + pub fn query (&self, event: &E) -> Option<&[Binding]> { + self.0.get(event).map(|x|x.as_slice()) + } + /// Return the first binding that corresponds to an event, considering conditions. + pub fn dispatch (&self, event: &E) -> Option<&Binding> { + self.query(event) + .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) + .flatten() + } +} + +impl Bind> { + pub fn load (lang: &impl Language) -> Usually { + let mut map = Bind::new(); + lang.each(|item|if item.expr().head() == Ok(Some("see")) { + // TODO Ok(()) - } - /// Write initial contents of a configuration file. - pub fn init_one ( - &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> - ) -> Usually<()> { - if self.dirs.find_config_file(path).is_none() { - //println!("Creating {path:?}"); - std::fs::write(self.dirs.place_config_file(path)?, defaults)?; - } - Ok(if let Some(path) = self.dirs.find_config_file(path) { - //println!("Loading {path:?}"); - let src = std::fs::read_to_string(&path)?; - src.as_str().each(move|item|each(self, item))?; - } else { - return Err(format!("{path}: not found").into()) - }) - } - /// Add statements to configuration from [Dsl] source. - pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { - dsl.each(|item|self.add_one(item))?; - Ok(self) - } - fn add_one (&self, item: impl Language) -> Usually<()> { - if let Some(expr) = item.expr()? { - let head = expr.head()?; - let tail = expr.tail()?; - let name = tail.head()?; - let body = tail.tail()?; - //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); - match head { - Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, - Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, - Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, - _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) - } + } else if let Ok(Some(_word)) = item.expr().head().word() { + if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { + map.add(key, Binding { + commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), + condition: None, + description: None, + source: None + }); Ok(()) - } else { - return Err(format!("Config::load: expected expr, got: {item:?}").into()) - } - } - pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { - self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() - } - } - - impl Mode> { - /// Add a definition to the mode. - /// - /// Supported definitions: - /// - /// - (name ...) -> name - /// - (info ...) -> description - /// - (keys ...) -> key bindings - /// - (mode ...) -> submode - /// - ... -> view - /// - /// ``` - /// let mut mode: tek::Mode> = Default::default(); - /// mode.add("(name hello)").unwrap(); - /// ``` - pub fn add (&mut self, dsl: impl Language) -> Usually<()> { - Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { - //println!("Mode::add: {head} {:?}", expr.tail()); - let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - match head { - "name" => self.add_name(tail)?, - "info" => self.add_info(tail)?, - "keys" => self.add_keys(tail)?, - "mode" => self.add_mode(tail)?, - _ => self.add_view(tail)?, - }; - } else if let Ok(Some(word)) = dsl.word() { - self.add_view(word); - } else { - return Err(format!("Mode::add: unexpected: {dsl:?}").into()); - }) - - //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) - //.word(|word|self.add_view(word)) - //.expr(|expr|expr.head(|head|{ - ////println!("Mode::add: {head} {:?}", expr.tail()); - //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - //match head { - //"name" => self.add_name(tail), - //"info" => self.add_info(tail), - //"keys" => self.add_keys(tail)?, - //"mode" => self.add_mode(tail)?, - //_ => self.add_view(tail), - //}; - //})) - } - - fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.name.push(src.into()))) - } - fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.info.push(src.into()))) - } - fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.view.push(src.into()))) - } - fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) - } - fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(if let Some(id) = dsl.head()? { - load_mode(&self.modes, &id, &dsl.tail())?; - } else { - return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); - })) - } - } - - impl Bind { - /// Create a new event map - pub fn new () -> Self { - Default::default() - } - /// Add a binding to an owned event map. - pub fn def (mut self, event: E, binding: Binding) -> Self { - self.add(event, binding); - self - } - /// Add a binding to an event map. - pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { - if !self.0.contains_key(&event) { - self.0.insert(event.clone(), Default::default()); - } - self.0.get_mut(&event).unwrap().push(binding); - self - } - /// Return the binding(s) that correspond to an event. - pub fn query (&self, event: &E) -> Option<&[Binding]> { - self.0.get(event).map(|x|x.as_slice()) - } - /// Return the first binding that corresponds to an event, considering conditions. - pub fn dispatch (&self, event: &E) -> Option<&Binding> { - self.query(event) - .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) - .flatten() - } - } - - impl Bind> { - pub fn load (lang: &impl Language) -> Usually { - let mut map = Bind::new(); - lang.each(|item|if item.expr().head() == Ok(Some("see")) { + } else if Some(":char") == item.expr()?.head()? { // TODO - Ok(()) - } else if let Ok(Some(_word)) = item.expr().head().word() { - if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { - map.add(key, Binding { - commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), - condition: None, - description: None, - source: None - }); - Ok(()) - } else if Some(":char") == item.expr()?.head()? { - // TODO - return Ok(()) - } else { - return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) - } + return Ok(()) } else { - return Err(format!("Config::load_bind: unexpected: {item:?}").into()) - })?; - Ok(map) + return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) + } + } else { + return Err(format!("Config::load_bind: unexpected: {item:?}").into()) + })?; + Ok(map) + } +} + +impl Dialog { + /// ``` + /// let _ = tek::Dialog::welcome(); + /// ``` + pub fn welcome () -> Self { + Self::Menu(1, MenuItems([ + MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), + MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ + app.dialog = Dialog::None; + app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); + })))), + MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), + ].into())) + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().menu_selected(); + /// ``` + pub fn menu_selected (&self) -> Option { + if let Self::Menu(selected, _) = self { Some(*selected) } else { None } + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().menu_next(); + /// ``` + pub fn menu_next (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), + _ => Self::None } } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().menu_prev(); + /// ``` + pub fn menu_prev (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().device_kind(); + /// ``` + pub fn device_kind (&self) -> Option { + if let Self::Device(index) = self { Some(*index) } else { None } + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().device_kind_next(); + /// ``` + pub fn device_kind_next (&self) -> Option { + self.device_kind().map(|index|(index + 1) % device_kinds().len()) + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().device_kind_prev(); + /// ``` + pub fn device_kind_prev (&self) -> Option { + self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) + } + /// FIXME: implement + pub fn message (&self) -> Option<&str> { todo!() } + /// FIXME: implement + pub fn browser (&self) -> Option<&Arc> { todo!() } + /// FIXME: implement + pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } +} +use crate::*; +impl_has!(Clock: |self: App|self.project.clock); +impl_has!(Vec: |self: App|self.project.midi_ins); +impl_has!(Vec: |self: App|self.project.midi_outs); +impl_has!(Dialog: |self: App|self.dialog); +impl_has!(Jack<'static>: |self: App|self.jack); +impl_has!(Measure: |self: App|self.size); +impl_has!(Pool: |self: App|self.pool); +impl_has!(Selection: |self: App|self.project.selection); +impl_as_ref!(Vec: |self: App|self.project.as_ref()); +impl_as_mut!(Vec: |self: App|self.project.as_mut()); +impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); +impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); +impl_has_clips!( |self: App|self.pool.clips); +impl_audio!(App: tek_jack_process, tek_jack_event); +impl_handle!(TuiIn: |self: App, input|{ + let commands = collect_commands(self, input)?; + let history = execute_commands(self, commands)?; + self.history.extend(history.into_iter()); + Ok(None) +}); + +impl Draw for App { + fn draw (self, to: &mut Tui) -> Usually> { + if let Some(e) = self.error.read().unwrap().as_ref() { + to.show(to.area(), e); + } + for (index, dsl) in self.mode.view.iter().enumerate() { + if let Err(e) = self.understand(to, dsl) { + *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); + break; + } + } + } +} + +impl<'a> Namespace<'a, AppCommand> for App { + symbols!('a |app| -> AppCommand { + "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, + "x/dec" => AppCommand::Dec { axis: ControlAxis::X }, + "y/inc" => AppCommand::Inc { axis: ControlAxis::Y }, + "y/dec" => AppCommand::Dec { axis: ControlAxis::Y }, + "confirm" => AppCommand::Confirm, + "cancel" => AppCommand::Cancel, + }); +} + +impl Understand for App { + fn understand_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { + app_understand_expr(self, to, lang) + } + fn understand_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { + app_understand_word(self, to, lang) + } +} + +fn app_understand_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> { + if evaluate_output_expression(state, to, lang)? + || evaluate_output_expression_tui(state, to, lang)? { + Ok(()) + } else { + Err(format!("App::understand_expr: unexpected: {lang:?}").into()) + } +} + +fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> { + let mut frags = dsl.src()?.unwrap().split("/"); + match frags.next() { + Some(":logo") => to.place(&view_logo()), + Some(":status") => to.place(&h_exact(1, "TODO: Status Bar")), + Some(":meters") => match frags.next() { + Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters")))), + Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters")))), + _ => panic!() + }, + Some(":tracks") => match frags.next() { + None => to.place(&"TODO tracks"), + Some("names") => to.place(&state.project.view_track_names(state.color.clone())),//Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), + Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs")))), + Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices")))), + Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs")))), + _ => panic!() + }, + Some(":scenes") => match frags.next() { + None => to.place(&"TODO scenes"), + Some(":scenes/names") => to.place(&"TODO Scene Names"), + _ => panic!() + }, + Some(":editor") => to.place(&"TODO Editor"), + Some(":dialog") => match frags.next() { + Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &state.dialog { + let items = items.clone(); + let selected = selected; + Some(wh_full(Thunk::new(move|to: &mut Tui|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + to.place(&y_push((2 * index) as u16, + Tui::fg_bg( + if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, + if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, + h_exact(2, origin_n(w_full(item))) + ))); + } + }))) + } else { + None + }), + _ => unimplemented!("App::understand_word: {dsl:?} ({frags:?})"), + }, + Some(":templates") => to.place(&{ + let modes = state.config.modes.clone(); + let height = (modes.read().unwrap().len() * 2) as u16; + h_exact(height, w_min(30, Thunk::new(move |to: &mut Tui|{ + for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { + let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; + let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); + let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); + let fg1 = Rgb(224, 192, 128); + let fg2 = Rgb(224, 128, 32); + let field_name = w_full(origin_w(Tui::fg(fg1, name))); + let field_id = w_full(origin_e(Tui::fg(fg2, id))); + let field_info = w_full(origin_w(info)); + to.place(&y_push((2 * index) as u16, + h_exact(2, w_full(Tui::bg(bg, south( + above(field_name, field_id), field_info)))))); + } + }))) + }), + Some(":sessions") => to.place(&h_exact(6, w_min(30, Thunk::new(|to: &mut Tui|{ + let fg = Rgb(224, 192, 128); + for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { + let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; + to.place(&y_push((2 * index) as u16, + &h_exact(2, w_full(Tui::bg(bg, origin_w(Tui::fg(fg, name))))))); + } + })))), + Some(":browse/title") => to.place(&w_full(origin_w(field_v(ItemColor::default(), + match state.dialog.browser_target().unwrap() { + BrowseTarget::SaveProject => "Save project:", + BrowseTarget::LoadProject => "Load project:", + BrowseTarget::ImportSample(_) => "Import sample:", + BrowseTarget::ExportSample(_) => "Export sample:", + BrowseTarget::ImportClip(_) => "Import clip:", + BrowseTarget::ExportClip(_) => "Export clip:", + }, w_shrink(3, h_exact(1, Tui::fg(Tui::g(96), x_repeat("🭻")))))))), + Some(":device") => { + let selected = state.dialog.device_kind().unwrap(); + to.place(&south(Tui::bold(true, "Add device"), iter_south(1, + move||device_kinds().iter(), + move|_label: &&'static str, i|{ + let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let lb = if i == selected { "[ " } else { " " }; + let rb = if i == selected { " ]" } else { " " }; + w_full(Tui::bg(bg, east(lb, west(rb, "FIXME device name")))) }))) + }, + Some(":debug") => to.place(&h_exact(1, format!("[{:?}]", to.area()))), + Some(_) => { + let views = state.config.views.read().unwrap(); + if let Some(dsl) = views.get(dsl.src()?.unwrap()) { + let dsl = dsl.clone(); + std::mem::drop(views); + state.understand(to, &dsl)? + } else { + unimplemented!("{dsl:?}"); + } + }, + _ => unreachable!() + } + Ok(()) +} + +impl App { + /// Update memoized render of clock values. + /// ``` + /// tek::App::default().update_clock(); + /// ``` + pub fn update_clock (&self) { + ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) + } + + /// Set modal dialog. + /// + /// ``` + /// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome()); + /// ``` + pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { + std::mem::swap(&mut self.dialog, &mut dialog); + dialog + } + + /// FIXME: generalize. Set picked device in device pick dialog. + /// + /// ``` + /// tek::App::default().device_pick(0); + /// ``` + pub fn device_pick (&mut self, index: usize) { + self.dialog = Dialog::Device(index); + } + + /// FIXME: generalize. Add device to current track. + pub fn add_device (&mut self, index: usize) -> Usually<()> { + match index { + 0 => { + let name = self.jack.with_client(|c|c.name().to_string()); + let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); + let track = self.track().expect("no active track"); + let port = format!("{}/Sampler", &track.name); + let connect = Connect::exact(format!("{name}:{midi}")); + let sampler = if let Ok(sampler) = Sampler::new( + &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] + ) { + self.dialog = Dialog::None; + Device::Sampler(sampler) + } else { + self.dialog = Dialog::Message("Failed to add device.".into()); + return Err("failed to add device".into()) + }; + let track = self.track_mut().expect("no active track"); + track.devices.push(sampler); + Ok(()) + }, + 1 => { + todo!(); + //Ok(()) + }, + _ => unreachable!(), + } + } + + /// Return reference to content browser if open. + /// + /// ``` + /// assert_eq!(tek::App::default().browser(), None); + /// ``` + pub fn browser (&self) -> Option<&Browse> { + if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + } + + /// Is a MIDI editor currently focused? + /// + /// ``` + /// tek::App::default().editor_focused(); + /// ``` + pub fn editor_focused (&self) -> bool { + false + } + + /// Toggle MIDI editor. + /// + /// ``` + /// tek::App::default().toggle_editor(None); + /// ``` + pub fn toggle_editor (&mut self, value: Option) { + //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); + let value = value.unwrap_or_else(||!self.editor().is_some()); + if value { + // Create new clip in pool when entering empty cell + if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && slot.is_none() + && let Some(track) = self.project.tracks.get_mut(track) + { + let (_index, clip) = self.pool.add_new_clip(); + // autocolor: new clip colors from scene and track color + let color = track.color.base.mix(scene.color.base, 0.5); + clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); + if let Some(editor) = &mut self.project.editor { + editor.set_clip(Some(&clip)); + } + *slot = Some(clip.clone()); + //Some(clip) + } else { + //None + } + } else if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && let Some(clip) = slot.as_mut() + { + // Remove clip from arrangement when exiting empty clip editor + let mut swapped = None; + if clip.read().unwrap().count_midi_messages() == 0 { + std::mem::swap(&mut swapped, slot); + } + if let Some(clip) = swapped { + self.pool.delete_clip(&clip.read().unwrap()); + } + } + } +} + +impl +AsMut> HasClock for T {} +impl +AsMut> HasSelection for T {} +impl +AsMut> HasSequencer for T {} +impl >+AsMut>> HasScenes for T {} +impl >+AsMut>> HasTracks for T {} +impl +AsMutOpt> HasEditor for T {} +impl +AsMutOpt+Send+Sync> HasScene for T {} +impl +AsMutOpt+Send+Sync> HasTrack for T {} +impl MidiPoint for T {} +impl > TracksView for T {} +impl MidiRange for T {} +impl ClipsView for T {} + +/// Default is always empty map regardless if `E` and `C` implement [Default]. +impl Default for Bind { + fn default () -> Self { Self(Default::default()) } +} +impl Default for Binding { + fn default () -> Self { + Self { + commands: Default::default(), + condition: Default::default(), + description: Default::default(), + source: Default::default(), + } + } +} + +impl_default!(AppCommand: Self::Nop); +impl_default!(MenuItem: Self("".into(), Arc::new(Box::new(|_|Ok(()))))); +impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ)); + +impl Gettable for AtomicBool { fn get (&self) -> bool { self.load(Relaxed) } } +impl InteriorMutable for AtomicBool { fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) } } +impl Gettable for AtomicUsize { fn get (&self) -> usize { self.load(Relaxed) } } +impl InteriorMutable for AtomicUsize { fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) } } + +impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } +impl AsRef> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } } +impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.size_inner } } +impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } + +#[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock); + +impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); +impl_debug!(Condition |self, w| { write!(w, "*") }); + +primitive!(u8: try_to_u8); +primitive!(u16: try_to_u16); +primitive!(usize: try_to_usize); +primitive!(isize: try_to_isize); +namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); +namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); +namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| { + ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), + ":h/sample-detail" => 6.max(app.measure_height() as u16 * 3 / 9), }; }); +namespace!(App: isize { literal = |dsl|try_to_isize(dsl); }); +namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| { + ":scene-count" => app.scenes().len(), + ":track-count" => app.tracks().len(), + ":device-kind" => app.dialog.device_kind().unwrap_or(0), + ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), + ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), }; }); +namespace!(App: bool { symbol = |app| { // Provide boolean values. + ":mode/editor" => app.project.editor.is_some(), + ":focused/dialog" => !matches!(app.dialog, Dialog::None), + ":focused/message" => matches!(app.dialog, Dialog::Message(..)), + ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), + ":focused/browser" => app.dialog.browser().is_some(), + ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), + ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), + ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), + ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), + ":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}), + ":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)), + ":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)), + ":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix), +}; }); +namespace!(App: ItemTheme {}); // TODO: provide colors here +namespace!(App: Dialog { symbol = |app| { + ":dialog/none" => Dialog::None, + ":dialog/options" => Dialog::Options, + ":dialog/device" => Dialog::Device(0), + ":dialog/device/prev" => Dialog::Device(0), + ":dialog/device/next" => Dialog::Device(0), + ":dialog/help" => Dialog::Help(0), + ":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, + Browse::new(None).unwrap().into()), + ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, + Browse::new(None).unwrap().into()), + ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), + Browse::new(None).unwrap().into()), +}; }); +namespace!(App: Selection { symbol = |app| { + ":select/scene" => app.selection().select_scene(app.tracks().len()), + ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), + ":select/scene/prev" => app.selection().select_scene_prev(), + ":select/track" => app.selection().select_track(app.tracks().len()), + ":select/track/next" => app.selection().select_track_next(app.tracks().len()), + ":select/track/prev" => app.selection().select_track_prev(), +}; }); +namespace!(App: Color { + symbol = |app| { + ":color/bg" => Color::Rgb(28, 32, 36), + }; + expression = |app| { + "g" (n: u8) => Color::Rgb(n, n, n), + "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), + }; +}); +namespace!(App: Option { symbol = |app| { + ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) +}; }); +namespace!(App: Option { symbol = |app| { + ":selected/scene" => app.selection().scene(), + ":selected/track" => app.selection().track(), +}; }); +namespace!(App: Option>> { + symbol = |app| { + ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { + app.scenes()[*scene].clips[*track].clone() + } else { + None + } + }; +}); diff --git a/src/tek_impls.rs b/src/tek_impls.rs deleted file mode 100644 index 3e245cfe..00000000 --- a/src/tek_impls.rs +++ /dev/null @@ -1,3586 +0,0 @@ -use crate::*; -use std::fmt::Write; - -/// Implement an arithmetic operation for a unit of time -#[macro_export] macro_rules! impl_op { - ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { - let $a = self.get(); let $b = other.get(); Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { - let $a = self.get(); let $b = other as f64; Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { - let $a = self.get(); let $b = other; Self($impl.into()) - } - } - } -} - -/// Define and implement a unit of time -#[macro_export] macro_rules! impl_time_unit { - ($T:ident) => { - impl Gettable for $T { - fn get (&self) -> f64 { self.0.load(Relaxed) } - } - impl InteriorMutable for $T { - fn set (&self, value: f64) -> f64 { - let old = self.get(); - self.0.store(value, Relaxed); - old - } - } - impl TimeUnit for $T {} - impl_op!($T, Add, add, |a, b|{a + b}); - impl_op!($T, Sub, sub, |a, b|{a - b}); - impl_op!($T, Mul, mul, |a, b|{a * b}); - impl_op!($T, Div, div, |a, b|{a / b}); - impl_op!($T, Rem, rem, |a, b|{a % b}); - impl From for $T { fn from (value: f64) -> Self { Self(value.into()) } } - impl From for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } } - impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } } - impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } } - impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } } - impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } } - impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } - } -} - -#[macro_export] macro_rules! impl_has_clips { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { - fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { - $cb.read().unwrap() - } - fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { - $cb.write().unwrap() - } - } - } -} - -impl +AsMut> HasClock for T {} -impl +AsMut> HasSelection for T {} -impl +AsMut> HasSequencer for T {} -impl >+AsMut>> HasScenes for T {} -impl >+AsMut>> HasTracks for T {} -impl +AsMutOpt> HasEditor for T {} -impl +AsMutOpt+Send+Sync> HasScene for T {} -impl +AsMutOpt+Send+Sync> HasTrack for T {} -impl MidiPoint for T {} -impl > TracksView for T {} -impl MidiRange for T {} -impl ClipsView for T {} - -mod app { - use crate::*; - impl_has!(Clock: |self: App|self.project.clock); - impl_has!(Vec: |self: App|self.project.midi_ins); - impl_has!(Vec: |self: App|self.project.midi_outs); - impl_has!(Dialog: |self: App|self.dialog); - impl_has!(Jack<'static>: |self: App|self.jack); - impl_has!(Measure: |self: App|self.size); - impl_has!(Pool: |self: App|self.pool); - impl_has!(Selection: |self: App|self.project.selection); - impl_as_ref!(Vec: |self: App|self.project.as_ref()); - impl_as_mut!(Vec: |self: App|self.project.as_mut()); - impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); - impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); - impl_has_clips!( |self: App|self.pool.clips); - impl_audio!(App: tek_jack_process, tek_jack_event); - impl_handle!(TuiIn: |self: App, input|{ - let commands = collect_commands(self, input)?; - let history = execute_commands(self, commands)?; - self.history.extend(history.into_iter()); - Ok(None) - }); - - impl Draw for App { - fn draw (self, to: &mut Tui) -> Usually> { - if let Some(e) = self.error.read().unwrap().as_ref() { - to.show(to.area(), e); - } - for (index, dsl) in self.mode.view.iter().enumerate() { - if let Err(e) = self.understand(to, dsl) { - *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); - break; - } - } - } - } - - impl<'a> Namespace<'a, AppCommand> for App { - symbols!('a |app| -> AppCommand { - "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, - "x/dec" => AppCommand::Dec { axis: ControlAxis::X }, - "y/inc" => AppCommand::Inc { axis: ControlAxis::Y }, - "y/dec" => AppCommand::Dec { axis: ControlAxis::Y }, - "confirm" => AppCommand::Confirm, - "cancel" => AppCommand::Cancel, - }); - } - - impl Understand for App { - fn understand_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { - app_understand_expr(self, to, lang) - } - fn understand_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { - app_understand_word(self, to, lang) - } - } - - fn app_understand_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> { - if evaluate_output_expression(state, to, lang)? - || evaluate_output_expression_tui(state, to, lang)? { - Ok(()) - } else { - Err(format!("App::understand_expr: unexpected: {lang:?}").into()) - } - } - - fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> { - let mut frags = dsl.src()?.unwrap().split("/"); - match frags.next() { - Some(":logo") => to.place(&view_logo()), - Some(":status") => to.place(&h_exact(1, "TODO: Status Bar")), - Some(":meters") => match frags.next() { - Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters")))), - Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters")))), - _ => panic!() - }, - Some(":tracks") => match frags.next() { - None => to.place(&"TODO tracks"), - Some("names") => to.place(&state.project.view_track_names(state.color.clone())),//Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), - Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs")))), - Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices")))), - Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs")))), - _ => panic!() - }, - Some(":scenes") => match frags.next() { - None => to.place(&"TODO scenes"), - Some(":scenes/names") => to.place(&"TODO Scene Names"), - _ => panic!() - }, - Some(":editor") => to.place(&"TODO Editor"), - Some(":dialog") => match frags.next() { - Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &state.dialog { - let items = items.clone(); - let selected = selected; - Some(wh_full(Thunk::new(move|to: &mut Tui|{ - for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - to.place(&y_push((2 * index) as u16, - Tui::fg_bg( - if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, - if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, - h_exact(2, origin_n(w_full(item))) - ))); - } - }))) - } else { - None - }), - _ => unimplemented!("App::understand_word: {dsl:?} ({frags:?})"), - }, - Some(":templates") => to.place(&{ - let modes = state.config.modes.clone(); - let height = (modes.read().unwrap().len() * 2) as u16; - h_exact(height, w_min(30, Thunk::new(move |to: &mut Tui|{ - for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { - let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; - let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); - let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); - let fg1 = Rgb(224, 192, 128); - let fg2 = Rgb(224, 128, 32); - let field_name = w_full(origin_w(Tui::fg(fg1, name))); - let field_id = w_full(origin_e(Tui::fg(fg2, id))); - let field_info = w_full(origin_w(info)); - to.place(&y_push((2 * index) as u16, - h_exact(2, w_full(Tui::bg(bg, south( - above(field_name, field_id), field_info)))))); - } - }))) - }), - Some(":sessions") => to.place(&h_exact(6, w_min(30, Thunk::new(|to: &mut Tui|{ - let fg = Rgb(224, 192, 128); - for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; - to.place(&y_push((2 * index) as u16, - &h_exact(2, w_full(Tui::bg(bg, origin_w(Tui::fg(fg, name))))))); - } - })))), - Some(":browse/title") => to.place(&w_full(origin_w(field_v(ItemColor::default(), - match state.dialog.browser_target().unwrap() { - BrowseTarget::SaveProject => "Save project:", - BrowseTarget::LoadProject => "Load project:", - BrowseTarget::ImportSample(_) => "Import sample:", - BrowseTarget::ExportSample(_) => "Export sample:", - BrowseTarget::ImportClip(_) => "Import clip:", - BrowseTarget::ExportClip(_) => "Export clip:", - }, w_shrink(3, h_exact(1, Tui::fg(Tui::g(96), x_repeat("🭻")))))))), - Some(":device") => { - let selected = state.dialog.device_kind().unwrap(); - to.place(&south(Tui::bold(true, "Add device"), iter_south(1, - move||device_kinds().iter(), - move|_label: &&'static str, i|{ - let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let lb = if i == selected { "[ " } else { " " }; - let rb = if i == selected { " ]" } else { " " }; - w_full(Tui::bg(bg, east(lb, west(rb, "FIXME device name")))) }))) - }, - Some(":debug") => to.place(&h_exact(1, format!("[{:?}]", to.area()))), - Some(_) => { - let views = state.config.views.read().unwrap(); - if let Some(dsl) = views.get(dsl.src()?.unwrap()) { - let dsl = dsl.clone(); - std::mem::drop(views); - state.understand(to, &dsl)? - } else { - unimplemented!("{dsl:?}"); - } - }, - _ => unreachable!() - } - Ok(()) - } - - impl App { - /// Update memoized render of clock values. - /// ``` - /// tek::App::default().update_clock(); - /// ``` - pub fn update_clock (&self) { - ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) - } - - /// Set modal dialog. - /// - /// ``` - /// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome()); - /// ``` - pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { - std::mem::swap(&mut self.dialog, &mut dialog); - dialog - } - - /// FIXME: generalize. Set picked device in device pick dialog. - /// - /// ``` - /// tek::App::default().device_pick(0); - /// ``` - pub fn device_pick (&mut self, index: usize) { - self.dialog = Dialog::Device(index); - } - - /// FIXME: generalize. Add device to current track. - pub fn add_device (&mut self, index: usize) -> Usually<()> { - match index { - 0 => { - let name = self.jack.with_client(|c|c.name().to_string()); - let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); - let track = self.track().expect("no active track"); - let port = format!("{}/Sampler", &track.name); - let connect = Connect::exact(format!("{name}:{midi}")); - let sampler = if let Ok(sampler) = Sampler::new( - &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] - ) { - self.dialog = Dialog::None; - Device::Sampler(sampler) - } else { - self.dialog = Dialog::Message("Failed to add device.".into()); - return Err("failed to add device".into()) - }; - let track = self.track_mut().expect("no active track"); - track.devices.push(sampler); - Ok(()) - }, - 1 => { - todo!(); - //Ok(()) - }, - _ => unreachable!(), - } - } - - /// Return reference to content browser if open. - /// - /// ``` - /// assert_eq!(tek::App::default().browser(), None); - /// ``` - pub fn browser (&self) -> Option<&Browse> { - if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } - } - - /// Is a MIDI editor currently focused? - /// - /// ``` - /// tek::App::default().editor_focused(); - /// ``` - pub fn editor_focused (&self) -> bool { - false - } - - /// Toggle MIDI editor. - /// - /// ``` - /// tek::App::default().toggle_editor(None); - /// ``` - pub fn toggle_editor (&mut self, value: Option) { - //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); - let value = value.unwrap_or_else(||!self.editor().is_some()); - if value { - // Create new clip in pool when entering empty cell - if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && slot.is_none() - && let Some(track) = self.project.tracks.get_mut(track) - { - let (_index, clip) = self.pool.add_new_clip(); - // autocolor: new clip colors from scene and track color - let color = track.color.base.mix(scene.color.base, 0.5); - clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); - if let Some(editor) = &mut self.project.editor { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip.clone()); - //Some(clip) - } else { - //None - } - } else if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && let Some(clip) = slot.as_mut() - { - // Remove clip from arrangement when exiting empty clip editor - let mut swapped = None; - if clip.read().unwrap().count_midi_messages() == 0 { - std::mem::swap(&mut swapped, slot); - } - if let Some(clip) = swapped { - self.pool.delete_clip(&clip.read().unwrap()); - } - } - } - } -} - -#[cfg(feature = "vst2")] impl ::vst::host::Host for Plugin {} - -mod arrange { - use crate::*; - impl_has!(Jack<'static>: |self: Arrangement| self.jack); - impl_has!(Measure: |self: Arrangement| self.size); - impl_has!(Vec: |self: Arrangement| self.tracks); - impl_has!(Vec: |self: Arrangement| self.scenes); - impl_has!(Vec: |self: Arrangement| self.midi_ins); - impl_has!(Vec: |self: Arrangement| self.midi_outs); - impl_has!(Clock: |self: Arrangement| self.clock); - impl_has!(Selection: |self: Arrangement| self.selection); - impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); - impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); - impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); - impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); - impl Arrangement { - /// Create a new arrangement. - pub fn new ( - jack: &Jack<'static>, - name: Option>, - clock: Clock, - tracks: Vec, - scenes: Vec, - midi_ins: Vec, - midi_outs: Vec, - ) -> Self { - Self { - clock, tracks, scenes, midi_ins, midi_outs, - jack: jack.clone(), - name: name.unwrap_or_default(), - color: ItemTheme::random(), - selection: Selection::TrackClip { track: 0, scene: 0 }, - ..Default::default() - } - } - /// Width of display - pub fn w (&self) -> u16 { - self.size.w() as u16 - } - /// Width allocated for sidebar. - pub fn w_sidebar (&self, is_editing: bool) -> u16 { - self.w() / if is_editing { 16 } else { 8 } as u16 - } - /// Width available to display tracks. - pub fn w_tracks_area (&self, is_editing: bool) -> u16 { - self.w().saturating_sub(self.w_sidebar(is_editing)) - } - /// Height of display - pub fn h (&self) -> u16 { - self.size.h() as u16 - } - /// Height taken by visible device slots. - pub fn h_devices (&self) -> u16 { - 2 - //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) - } - /// Add multiple tracks - #[cfg(feature = "track")] pub fn tracks_add ( - &mut self, - count: usize, width: Option, - mins: &[Connect], mouts: &[Connect], - ) -> Usually<()> { - 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 track = self.track_add(None, Some(color), mins, mouts)?.1; - if let Some(width) = width { - track.width = width; - } - } - Ok(()) - } - /// Add a track - #[cfg(feature = "track")] pub fn track_add ( - &mut self, - name: Option<&str>, color: Option, - mins: &[Connect], mouts: &[Connect], - ) -> Usually<(usize, &mut Track)> { - let name: Arc = name.map_or_else( - ||format!("trk{:02}", self.track_last).into(), - |x|x.to_string().into() - ); - self.track_last += 1; - let track = Track { - width: (name.len() + 2).max(12), - color: color.unwrap_or_else(ItemTheme::random), - sequencer: Sequencer::new( - &format!("{name}"), - self.jack(), - Some(self.clock()), - None, - mins, - mouts - )?, - name, - ..Default::default() - }; - self.tracks_mut().push(track); - let len = self.tracks().len(); - let index = len - 1; - for scene in self.scenes_mut().iter_mut() { - while scene.clips.len() < len { - scene.clips.push(None); - } - } - Ok((index, &mut self.tracks_mut()[index])) - } - #[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { - south( - h_exact(1, self.view_inputs_header()), - Thunk::new(|to: &mut Tui|{ - for (index, port) in self.midi_ins().iter().enumerate() { - to.place(&x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port)))) - } - }) - ) - } - #[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Draw + '_ { - east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), - west(w_exact(4, button_2("I", "+", false)), Thunk::new(move|to: &mut Tui|for (_index, track, x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - to.place(&x_push(x1 as u16, Tui::bg(track.color.dark.rgb, origin_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), - either(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), - either(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), - )))))) - }))) - } - #[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Draw { - east(w_exact(20, origin_w(east(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), - west(w_exact(4, ()), Thunk::new(move|to: &mut Tui|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - to.place(&Tui::bg(track.color.darker.rgb, origin_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), - either(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), - either(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), - ))))) - }))) - } - #[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { - let mut h = 1; - for output in self.midi_outs().iter() { - h += 1 + output.connections.len(); - } - let h = h as u16; - let list = south( - h_exact(1, w_full(origin_w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), - h_exact(h - 1, wh_full(origin_nw(Thunk::new(|to: &mut Tui|{ - for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&h_exact(1,w_full(east( - origin_w(east(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), - w_full(origin_e(format!("{}/{} ", - port.port().get_connections().len(), - port.connections.len()))))))); - for (index, conn) in port.connections.iter().enumerate() { - to.place(&h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info()))))); - } - } - }))))); - h_exact(h, view_track_row_section(theme, list, button_2("O", "+", false), - Tui::bg(theme.darker.rgb, origin_w(w_full( - Thunk::new(|to: &mut Tui|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&w_exact(track_width(index, track), - Thunk::new(|to: &mut Tui|{ - to.place(&h_exact(1, origin_w(east( - either(true, Tui::fg(Green, "play "), "play "), - either(false, Tui::fg(Yellow, "solo "), "solo "), - )))); - for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&h_exact(1, origin_w(east( - either(true, Tui::fg(Green, " ● "), " · "), - either(false, Tui::fg(Yellow, " ● "), " · "), - )))); - for (_index, _conn) in port.connections.iter().enumerate() { - to.place(&h_exact(1, w_full(""))); - } - }})))}})))))) - } - #[cfg(feature = "track")] pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { - let mut h = 2u16; - for track in self.tracks().iter() { - h = h.max(track.devices.len() as u16 * 2); - } - view_track_row_section(theme, - button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), - button_2("D", "+", false), - Thunk::new(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&wh_exact(track_width(index, track), h + 1, - Tui::bg(track.color.dark.rgb, origin_nw(iter_south(2, move||0..h, - |_, _index|wh_exact(track.width as u16, 2, - Tui::fg_bg( - ItemTheme::G[32].lightest.rgb, - ItemTheme::G[32].dark.rgb, - origin_nw(format!(" · {}", "--"))))))))); - })) - } - /// Put a clip in a slot - #[cfg(feature = "clip")] pub fn clip_put ( - &mut self, track: usize, scene: usize, clip: Option>> - ) -> Option>> { - let old = self.scenes[scene].clips[track].clone(); - self.scenes[scene].clips[track] = clip; - old - } - /// Change the color of a clip, returning the previous one - #[cfg(feature = "clip")] pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) - -> Option - { - self.scenes[scene].clips[track].as_ref().map(|clip|{ - let mut clip = clip.write().unwrap(); - let old = clip.color.clone(); - clip.color = color.clone(); - panic!("{color:?} {old:?}"); - //old - }) - } - /// Toggle looping for the active clip - #[cfg(feature = "clip")] pub fn toggle_loop (&mut self) { - if let Some(clip) = self.selected_clip() { - clip.write().unwrap().toggle_loop() - } - } - /// Get the first sampler of the active track - #[cfg(feature = "sampler")] pub fn sampler (&self) -> Option<&Sampler> { - self.selected_track()?.sampler(0) - } - /// Get the first sampler of the active track - #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.selected_track_mut()?.sampler_mut(0) - } - } - impl ScenesView for Arrangement { - fn h_scenes (&self) -> u16 { - (self.measure_height() as u16).saturating_sub(20) - } - fn w_side (&self) -> u16 { - (self.measure_width() as u16 * 2 / 10).max(20) - } - fn w_mid (&self) -> u16 { - (self.measure_width() as u16).saturating_sub(2 * self.w_side()).max(40) - } - } - impl HasClipsSize for Arrangement { - fn clips_size (&self) -> &Measure { &self.size_inner } - } -} - -mod browse { - use crate::*; - impl Browse { - pub fn new (cwd: Option) -> Usually { - let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; - let mut dirs = vec![]; - let mut files = vec![]; - for entry in std::fs::read_dir(&cwd)? { - let entry = entry?; - let name = entry.file_name(); - let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); - let meta = entry.metadata()?; - if meta.is_dir() { - dirs.push((name, format!("📁 {decoded}"))); - } else if meta.is_file() { - files.push((name, format!("📄 {decoded}"))); - } - } - Ok(Self { cwd, dirs, files, ..Default::default() }) - } - pub fn chdir (&self) -> Usually { Self::new(Some(self.path())) } - pub fn len (&self) -> usize { self.dirs.len() + self.files.len() } - pub fn is_dir (&self) -> bool { self.index < self.dirs.len() } - pub fn is_file (&self) -> bool { self.index >= self.dirs.len() } - pub fn path (&self) -> PathBuf { - self.cwd.join(if self.is_dir() { - &self.dirs[self.index].0 - } else if self.is_file() { - &self.files[self.index - self.dirs.len()].0 - } else { - unreachable!() - }) - } - fn _todo_stub_path_buf (&self) -> PathBuf { todo!() } - fn _todo_stub_usize (&self) -> usize { todo!() } - fn _todo_stub_arc_str (&self) -> Arc { todo!() } - } - impl Browse { - fn tui (&self) -> impl Draw { - iter_south(1, ||EntriesIterator { - offset: 0, - index: 0, - length: self.dirs.len() + self.files.len(), - browser: self, - }, |entry, _index|w_full(origin_w(entry))) - } - } - impl<'a> Iterator for EntriesIterator<'a> { - type Item = Modify<&'a str>; - fn next (&mut self) -> Option { - let dirs = self.browser.dirs.len(); - let files = self.browser.files.len(); - let index = self.index; - if self.index < dirs { - self.index += 1; - Some(Tui::bold(true, self.browser.dirs[index].1.as_str())) - } else if self.index < dirs + files { - self.index += 1; - Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str())) - } else { - None - } - } - } - impl PartialEq for BrowseTarget { - fn eq (&self, other: &Self) -> bool { - match self { - Self::ImportSample(_) => false, - Self::ExportSample(_) => false, - Self::ImportClip(_) => false, - Self::ExportClip(_) => false, - #[allow(unused)] t => matches!(other, t) - } - } - } -} - -mod clock { - use crate::*; - impl std::fmt::Debug for Clock { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Clock") - .field("timebase", &self.timebase) - .field("chunk", &self.chunk) - .field("quant", &self.quant) - .field("sync", &self.sync) - .field("global", &self.global) - .field("playhead", &self.playhead) - .field("started", &self.started) - .finish() - } - } - impl Clock { - pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { - let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); - let timebase = Arc::new(Timebase::default()); - let clock = Self { - quant: Arc::new(24.into()), - sync: Arc::new(384.into()), - transport: Arc::new(Some(transport)), - chunk: Arc::new((chunk as usize).into()), - global: Arc::new(Moment::zero(&timebase)), - playhead: Arc::new(Moment::zero(&timebase)), - offset: Arc::new(Moment::zero(&timebase)), - started: RwLock::new(None).into(), - timebase, - midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), - midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), - click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), - ..Default::default() - }; - if let Some(bpm) = bpm { - clock.timebase.bpm.set(bpm); - } - Ok(clock) - } - pub fn timebase (&self) -> &Arc { - &self.timebase - } - /// Current sample rate - pub fn sr (&self) -> &SampleRate { - &self.timebase.sr - } - /// Current tempo - pub fn bpm (&self) -> &Bpm { - &self.timebase.bpm - } - /// Current MIDI resolution - pub fn ppq (&self) -> &Ppq { - &self.timebase.ppq - } - /// Next pulse that matches launch sync (for phrase switchover) - pub fn next_launch_pulse (&self) -> usize { - let sync = self.sync.get() as usize; - let pulse = self.playhead.pulse.get() as usize; - if pulse % sync == 0 { - pulse - } else { - (pulse / sync + 1) * sync - } - } - /// Start playing, optionally seeking to a given location beforehand - pub fn play_from (&self, start: Option) -> Usually<()> { - if let Some(transport) = self.transport.as_ref() { - if let Some(start) = start { - transport.locate(start)?; - } - transport.start()?; - } - Ok(()) - } - /// Pause, optionally seeking to a given location afterwards - pub fn pause_at (&self, pause: Option) -> Usually<()> { - if let Some(transport) = self.transport.as_ref() { - transport.stop()?; - if let Some(pause) = pause { - transport.locate(pause)?; - } - } - Ok(()) - } - /// Is currently paused? - pub fn is_stopped (&self) -> bool { - self.started.read().unwrap().is_none() - } - /// Is currently playing? - pub fn is_rolling (&self) -> bool { - self.started.read().unwrap().is_some() - } - /// Update chunk size - pub fn set_chunk (&self, n_frames: usize) { - self.chunk.store(n_frames, Relaxed); - } - pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { - // Store buffer length - self.set_chunk(scope.n_frames() as usize); - - // Store reported global frame and usec - let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; - self.global.sample.set(current_frames as f64); - self.global.usec.set(current_usecs as f64); - - let mut started = self.started.write().unwrap(); - - // If transport has just started or just stopped, - // update starting point: - if let Some(transport) = self.transport.as_ref() { - match (transport.query_state()?, started.as_ref()) { - (TransportState::Rolling, None) => { - let moment = Moment::zero(&self.timebase); - moment.sample.set(current_frames as f64); - moment.usec.set(current_usecs as f64); - *started = Some(moment); - }, - (TransportState::Stopped, Some(_)) => { - *started = None; - }, - _ => {} - }; - } - - self.playhead.update_from_sample(started.as_ref() - .map(|started|current_frames as f64 - started.sample.get()) - .unwrap_or(0.)); - - Ok(()) - } - - pub fn bbt (&self) -> PositionBBT { - let pulse = self.playhead.pulse.get() as i32; - let ppq = self.timebase.ppq.get() as i32; - let bpm = self.timebase.bpm.get(); - let bar = (pulse / ppq) / 4; - PositionBBT { - bar: 1 + bar, - beat: 1 + (pulse / ppq) % 4, - tick: (pulse % ppq), - bar_start_tick: (bar * 4 * ppq) as f64, - beat_type: 4., - beats_per_bar: 4., - beats_per_minute: bpm, - ticks_per_beat: ppq as f64 - } - } - - pub fn next_launch_instant (&self) -> Moment { - Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) - } - - /// Get index of first sample to populate. - /// - /// Greater than 0 means that the first pulse of the clip - /// falls somewhere in the middle of the chunk. - pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ - (scope.last_frame_time() as usize).saturating_sub( - started.sample.get() as usize + - self.started.read().unwrap().as_ref().unwrap().sample.get() as usize - ) - } - - // Get iterator that emits sample paired with pulse. - // - // * Sample: index into output buffer at which to write MIDI event - // * Pulse: index into clip from which to take the MIDI event - // - // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. - pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> Ticker { - self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) - } - } - impl Clock { - fn _todo_provide_u32 (&self) -> u32 { - todo!() - } - fn _todo_provide_opt_u32 (&self) -> Option { - todo!() - } - fn _todo_provide_f64 (&self) -> f64 { - todo!() - } - } - impl Command for ClockCommand { - fn execute (&self, state: &mut T) -> Perhaps { - self.execute(state.clock_mut()) // awesome - } - } - impl ClockView { - pub const BEAT_EMPTY: &'static str = "-.-.--"; - pub const TIME_EMPTY: &'static str = "-.---s"; - pub const BPM_EMPTY: &'static str = "---.---"; - pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { - let rate = clock.timebase.sr.get(); - let chunk = clock.chunk.load(Relaxed) as f64; - let lat = chunk / rate * 1000.; - let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); - let mut cache = cache.write().unwrap(); - cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); - cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); - cache.sr.update(Some((compact, rate)), |buf,_,_|{ - buf.clear(); - if compact { - write!(buf, "{:.1}kHz", rate / 1000.) - } else { - write!(buf, "{:.0}Hz", rate) - } - }); - if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { - let pulse = clock.timebase.usecs_to_pulse(now); - let time = now/1000000.; - let bpm = clock.timebase.bpm.get(); - cache.beat.update(Some(pulse), |buf, _, _|{ - buf.clear(); - clock.timebase.format_beats_1_to(buf, pulse) - }); - cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); - cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); - } else { - cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY)); - cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY)); - cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY)); - } - } - } - impl_default!(ClockView: { - let mut beat = String::with_capacity(16); - let _ = write!(beat, "{}", Self::BEAT_EMPTY); - let mut time = String::with_capacity(16); - let _ = write!(time, "{}", Self::TIME_EMPTY); - let mut bpm = String::with_capacity(16); - let _ = write!(bpm, "{}", Self::BPM_EMPTY); - Self { - beat: Memo::new(None, beat), - time: Memo::new(None, time), - bpm: Memo::new(None, bpm), - sr: Memo::new(None, String::with_capacity(16)), - buf: Memo::new(None, String::with_capacity(16)), - lat: Memo::new(None, String::with_capacity(16)), - } - }); -} - -impl Selection { - pub fn describe ( - &self, - #[cfg(feature = "track")] tracks: &[Track], - #[cfg(feature = "scene")] scenes: &[Scene], - ) -> Arc { - use Selection::*; - format!("{}", match self { - Mix => "Everything".to_string(), - #[cfg(feature = "scene")] Scene(s) => - scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), - #[cfg(feature = "track")] Track(t) => - tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), - TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { - (Some(_), Some(s)) => match s.clip(*track) { - Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), - None => format!("T{track} S{scene}: Empty") - }, - _ => format!("T{track} S{scene}: Empty"), - }, - _ => todo!() - }).into() - } - #[cfg(feature = "scene")] pub fn scene (&self) -> Option { - use Selection::*; - match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } - } - #[cfg(feature = "scene")] pub fn select_scene (&self, scene_count: usize) -> Self { - use Selection::*; - match self { - Mix | Track(_) => Scene(0), - Scene(s) => Scene((s + 1) % scene_count), - TrackClip { scene, .. } => Track(*scene), - _ => todo!(), - } - } - #[cfg(feature = "scene")] pub fn select_scene_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Scene(0), - Track(t) => TrackClip { track: *t, scene: 0 }, - Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, - TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, - _ => todo!() - } - } - #[cfg(feature = "scene")] pub fn select_scene_prev (&self) -> Self { - use Selection::*; - match self { - Mix | Scene(0) => Mix, - Scene(s) => Scene(s - 1), - Track(t) => Track(*t), - TrackClip { track, scene: 0 } => Track(*track), - TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, - _ => todo!() - } - } - #[cfg(feature = "track")] pub fn track (&self) -> Option { - use Selection::*; - if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { - Some(*track) - } else { - None - } - } - #[cfg(feature = "track")] pub fn select_track (&self, track_count: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(_) => Mix, - Track(t) => Track((t + 1) % track_count), - TrackClip { track, .. } => Track(*track), - _ => todo!(), - } - } - #[cfg(feature = "track")] pub fn select_track_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(s) => TrackClip { track: 0, scene: *s }, - Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, - TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, - _ => todo!() - } - } - #[cfg(feature = "track")] pub fn select_track_prev (&self) -> Self { - use Selection::*; - match self { - Mix => Mix, - Scene(s) => Scene(*s), - Track(0) => Mix, - Track(t) => Track(t - 1), - TrackClip { track: 0, scene } => Scene(*scene), - TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, - _ => todo!() - } - } -} - - - -/// Default is always empty map regardless if `E` and `C` implement [Default]. -impl Default for Bind { - fn default () -> Self { Self(Default::default()) } -} -impl Default for Binding { - fn default () -> Self { - Self { - commands: Default::default(), - condition: Default::default(), - description: Default::default(), - source: Default::default(), - } - } -} - -impl_default!(AppCommand: Self::Nop); -impl_default!(MenuItem: Self("".into(), Arc::new(Box::new(|_|Ok(()))))); -impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ)); - -impl Gettable for AtomicBool { fn get (&self) -> bool { self.load(Relaxed) } } -impl InteriorMutable for AtomicBool { fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) } } -impl Gettable for AtomicUsize { fn get (&self) -> usize { self.load(Relaxed) } } -impl InteriorMutable for AtomicUsize { fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) } } - -impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } -impl AsRef> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } } -impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.size_inner } } -impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } - -#[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock); - -impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); -impl_debug!(Condition |self, w| { write!(w, "*") }); - -primitive!(u8: try_to_u8); -primitive!(u16: try_to_u16); -primitive!(usize: try_to_usize); -primitive!(isize: try_to_isize); -namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); -namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); -namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| { - ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), - ":h/sample-detail" => 6.max(app.measure_height() as u16 * 3 / 9), }; }); -namespace!(App: isize { literal = |dsl|try_to_isize(dsl); }); -namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| { - ":scene-count" => app.scenes().len(), - ":track-count" => app.tracks().len(), - ":device-kind" => app.dialog.device_kind().unwrap_or(0), - ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), - ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), }; }); -namespace!(App: bool { symbol = |app| { // Provide boolean values. - ":mode/editor" => app.project.editor.is_some(), - ":focused/dialog" => !matches!(app.dialog, Dialog::None), - ":focused/message" => matches!(app.dialog, Dialog::Message(..)), - ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), - ":focused/browser" => app.dialog.browser().is_some(), - ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), - ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), - ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), - ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), - ":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}), - ":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)), - ":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)), - ":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix), -}; }); -namespace!(App: ItemTheme {}); // TODO: provide colors here -namespace!(App: Dialog { symbol = |app| { - ":dialog/none" => Dialog::None, - ":dialog/options" => Dialog::Options, - ":dialog/device" => Dialog::Device(0), - ":dialog/device/prev" => Dialog::Device(0), - ":dialog/device/next" => Dialog::Device(0), - ":dialog/help" => Dialog::Help(0), - ":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, - Browse::new(None).unwrap().into()), - ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, - Browse::new(None).unwrap().into()), - ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), - Browse::new(None).unwrap().into()), -}; }); -namespace!(App: Selection { symbol = |app| { - ":select/scene" => app.selection().select_scene(app.tracks().len()), - ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), - ":select/scene/prev" => app.selection().select_scene_prev(), - ":select/track" => app.selection().select_track(app.tracks().len()), - ":select/track/next" => app.selection().select_track_next(app.tracks().len()), - ":select/track/prev" => app.selection().select_track_prev(), -}; }); -namespace!(App: Color { - symbol = |app| { - ":color/bg" => Color::Rgb(28, 32, 36), - }; - expression = |app| { - "g" (n: u8) => Color::Rgb(n, n, n), - "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), - }; -}); -namespace!(App: Option { symbol = |app| { - ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) -}; }); -namespace!(App: Option { symbol = |app| { - ":selected/scene" => app.selection().scene(), - ":selected/track" => app.selection().track(), -}; }); -namespace!(App: Option>> { - symbol = |app| { - ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { - app.scenes()[*scene].clips[*track].clone() - } else { - None - } - }; -}); - -mod time { - use crate::*; - impl Moment { - pub fn zero (timebase: &Arc) -> Self { - Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } - } - pub fn from_usec (timebase: &Arc, usec: f64) -> Self { - Self { - usec: usec.into(), - sample: timebase.sr.usecs_to_sample(usec).into(), - pulse: timebase.usecs_to_pulse(usec).into(), - timebase: timebase.clone(), - } - } - pub fn from_sample (timebase: &Arc, sample: f64) -> Self { - Self { - sample: sample.into(), - usec: timebase.sr.samples_to_usec(sample).into(), - pulse: timebase.samples_to_pulse(sample).into(), - timebase: timebase.clone(), - } - } - pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { - Self { - pulse: pulse.into(), - sample: timebase.pulses_to_sample(pulse).into(), - usec: timebase.pulses_to_usec(pulse).into(), - timebase: timebase.clone(), - } - } - #[inline] pub fn update_from_usec (&self, usec: f64) { - self.usec.set(usec); - self.pulse.set(self.timebase.usecs_to_pulse(usec)); - self.sample.set(self.timebase.sr.usecs_to_sample(usec)); - } - #[inline] pub fn update_from_sample (&self, sample: f64) { - self.usec.set(self.timebase.sr.samples_to_usec(sample)); - self.pulse.set(self.timebase.samples_to_pulse(sample)); - self.sample.set(sample); - } - #[inline] pub fn update_from_pulse (&self, pulse: f64) { - self.usec.set(self.timebase.pulses_to_usec(pulse)); - self.pulse.set(pulse); - self.sample.set(self.timebase.pulses_to_sample(pulse)); - } - #[inline] pub fn format_beat (&self) -> Arc { - self.timebase.format_beats_1(self.pulse.get()).into() - } - } - impl LaunchSync { - pub fn next (&self) -> f64 { - note_duration_next(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - note_duration_prev(self.get() as usize) as f64 - } - } - impl Quantize { - pub fn next (&self) -> f64 { - note_duration_next(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - note_duration_prev(self.get() as usize) as f64 - } - } - impl Timebase { - /// Specify sample rate, BPM and PPQ - pub fn new ( - s: impl Into, - b: impl Into, - p: impl Into - ) -> Self { - Self { sr: s.into(), bpm: b.into(), ppq: p.into() } - } - /// Iterate over ticks between start and end. - #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> Ticker { - Ticker { spp: self.samples_per_pulse(), sample: start, start, end } - } - /// Return the duration fo a beat in microseconds - #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } - /// Return the number of beats in a second - #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } - /// Return the number of microseconds corresponding to a note of the given duration - #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { - 4.0 * self.usec_per_beat() * num / den - } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } - /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) - #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } - /// Return number of pulses in a second (BPM-dependent) - #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } - /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) - #[inline] pub fn pulses_per_sample (&self) -> f64 { - self.usec_per_pulse() / self.sr.usec_per_sample() - } - /// Return number of samples in a pulse (SR- and BPM-dependent) - #[inline] pub fn samples_per_pulse (&self) -> f64 { - self.sr.get() / self.pulses_per_second() - } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { - self.pulses_per_sample() * p - } - /// Convert a number of samples to a pulse number (SR- and BPM-dependent) - #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { - s / self.pulses_per_sample() - } - /// Return the number of samples corresponding to a note of the given duration - #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { - self.usec_to_sample(self.note_to_usec(note)) - } - /// Return the number of samples corresponding to the given number of microseconds - #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { - usec * self.sr.get() / 1000f64 - } - /// Return the quantized position of a moment in time given a step - #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { - let step = self.note_to_usec(step); - (time / step, time % step) - } - /// Quantize a collection of events - #[inline] pub fn quantize_into + Sized, T> ( - &self, step: (f64, f64), events: E - ) -> Vec<(f64, f64)> { - events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 0 - #[inline] pub fn format_beats_0 (&self, pulse: f64) -> Arc { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - format!("{}.{}.{pulses:02}", beats / 4, beats % 4).into() - } - /// Format a number of pulses into Beat.Bar starting from 0 - #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4, beats % 4).into() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc { - let mut string = String::with_capacity(16); - self.format_beats_1_to(&mut string, pulse).expect("failed to format {pulse} into beat"); - string.into() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1_to (&self, w: &mut impl std::fmt::Write, pulse: f64) -> Result<(), std::fmt::Error> { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - write!(w, "{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4 + 1, beats % 4 + 1).into() - } - } - impl SampleRate { - /// Return the duration of a sample in microseconds (floating) - #[inline] pub fn usec_per_sample (&self) -> f64 { - 1_000_000f64 / self.get() - } - /// Return the duration of a sample in microseconds (floating) - #[inline] pub fn sample_per_usec (&self) -> f64 { - self.get() / 1_000_000f64 - } - /// Convert a number of samples to microseconds (floating) - #[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 { - self.usec_per_sample() * samples - } - /// Convert a number of microseconds to samples (floating) - #[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 { - self.sample_per_usec() * usecs - } - } - impl Microsecond { - #[inline] pub fn format_msu (&self) -> Arc { - let usecs = self.get() as usize; - let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); - let (minutes, seconds) = (seconds / 60, seconds % 60); - format!("{minutes}:{seconds:02}:{msecs:03}").into() - } - } - - impl_time_unit!(SampleCount); - impl_time_unit!(SampleRate); - impl_time_unit!(Microsecond); - impl_time_unit!(Quantize); - impl_time_unit!(Ppq); - impl_time_unit!(Pulse); - impl_time_unit!(Bpm); - impl_time_unit!(LaunchSync); -} - -mod midi { - use crate::*; - impl_from!(MidiSelection: |data:(usize, bool)| Self { - time_len: Arc::new(0.into()), - note_axis: Arc::new(0.into()), - note_lo: Arc::new(0.into()), - time_axis: Arc::new(0.into()), - time_start: Arc::new(0.into()), - time_zoom: Arc::new(data.0.into()), - time_lock: Arc::new(data.1.into()), - }); - impl_default!(MidiCursor: Self { - time_pos: Arc::new(0.into()), - note_pos: Arc::new(36.into()), - note_len: Arc::new(24.into()), - }); - - impl NotePoint for MidiCursor { - fn note_len (&self) -> &AtomicUsize { - &self.note_len - } - fn note_pos (&self) -> &AtomicUsize { - &self.note_pos - } - } - - impl TimePoint for MidiCursor { - fn time_pos (&self) -> &AtomicUsize { - self.time_pos.as_ref() - } - } - - impl TimeRange for MidiSelection { - fn time_len (&self) -> &AtomicUsize { &self.time_len } - fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } - fn time_lock (&self) -> &AtomicBool { &self.time_lock } - fn time_start (&self) -> &AtomicUsize { &self.time_start } - fn time_axis (&self) -> &AtomicUsize { &self.time_axis } - } - - impl NoteRange for MidiSelection { - fn note_lo (&self) -> &AtomicUsize { &self.note_lo } - fn note_axis (&self) -> &AtomicUsize { &self.note_axis } - } - - impl Iterator for Ticker { - type Item = (usize, usize); - fn next (&mut self) -> Option { - loop { - if self.sample > self.end { return None } - let spp = self.spp; - let sample = self.sample as f64; - let start = self.start; - let end = self.end; - self.sample += 1; - //println!("{spp} {sample} {start} {end}"); - let jitter = sample.rem_euclid(spp); // ramps - let next_jitter = (sample + 1.0).rem_euclid(spp); - if jitter > next_jitter { // at crossing: - let time = (sample as usize) % (end as usize-start as usize); - let tick = (sample / spp) as usize; - return Some((time, tick)) - } - } - } - } - - impl JackPort for MidiInput { - type Port = MidiIn; - type Pair = MidiOut; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec(), - held: Arc::new(RwLock::new([false;128])) - }; - port.connect_to_matching()?; - Ok(port) - } - } - - impl JackPort for MidiOutput { - type Port = MidiOut; - type Pair = MidiIn; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self::register(jack, name)?; - let jack = jack.clone(); - let name = name.as_ref().into(); - let connections = connect.to_vec(); - let port = Self { - jack, - port, - name, - connections, - held: Arc::new([false;128].into()), - note_buffer: vec![0;8], - output_buffer: vec![vec![];65536], - }; - port.connect_to_matching()?; - Ok(port) - } - } - - impl MidiOutput { - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { - let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); - for frame in &mut self.output_buffer[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(&mut self.output_buffer); - } - } - /// Write a note to the output buffer - pub fn buffer_write <'a> ( - &'a mut self, - sample: usize, - event: LiveEvent, - ) { - self.note_buffer.fill(0); - event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); - self.output_buffer[sample].push(self.note_buffer.clone()); - // Update the list of currently held notes. - if let LiveEvent::Midi { ref message, .. } = event { - update_keys(&mut*self.held.write().unwrap(), message); - } - } - /// Write a chunk of MIDI data from the output buffer to the output port. - pub fn buffer_emit (&mut self, scope: &ProcessScope) { - let samples = scope.n_frames() as usize; - let mut writer = self.port.writer(scope); - for (time, events) in self.output_buffer.iter().enumerate().take(samples) { - for bytes in events.iter() { - writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ - panic!("Failed to write MIDI data: {bytes:?}"); - }); - } - } - } - } - impl MidiInput { - pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { - parse_midi_input(self.port().iter(scope)) - } - } - impl> + AsMut>> HasMidiIns for T { - fn midi_ins (&self) -> &Vec { self.as_ref() } - fn midi_ins_mut (&mut self) -> &mut Vec { self.as_mut() } - } - impl> + AsMut>> HasMidiOuts for T { - fn midi_outs (&self) -> &Vec { self.as_ref() } - fn midi_outs_mut (&mut self) -> &mut Vec { self.as_mut() } - } - impl> AddMidiIn for T { - fn midi_in_add (&mut self) -> Usually<()> { - let index = self.midi_ins().len(); - let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; - self.midi_ins_mut().push(port); - Ok(()) - } - } - /// Trail for thing that may gain new MIDI ports. - impl> AddMidiOut for T { - fn midi_out_add (&mut self) -> Usually<()> { - let index = self.midi_outs().len(); - let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; - self.midi_outs_mut().push(port); - Ok(()) - } - } - - impl MidiClip { - pub fn new ( - name: impl AsRef, - looped: bool, - length: usize, - notes: Option, - color: Option, - ) -> Self { - Self { - uuid: uuid::Uuid::new_v4(), - name: name.as_ref().into(), - ppq: PPQ, - length, - notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), - looped, - loop_start: 0, - loop_length: length, - percussive: true, - color: color.unwrap_or_else(ItemTheme::random) - } - } - pub fn count_midi_messages (&self) -> usize { - let mut count = 0; - for tick in self.notes.iter() { - count += tick.len(); - } - count - } - pub fn set_length (&mut self, length: usize) { - self.length = length; - self.notes = vec![Vec::with_capacity(16);length]; - } - pub fn duplicate (&self) -> Self { - let mut clone = self.clone(); - clone.uuid = uuid::Uuid::new_v4(); - clone - } - pub fn toggle_loop (&mut self) { self.looped = !self.looped; } - pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { - if pulse >= self.length { panic!("extend clip first") } - self.notes[pulse].push(message); - } - /// Check if a range `start..end` contains MIDI Note On `k` - pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { - for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { - for event in events.iter() { - if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } - } - } - false - } - pub fn stop_all () -> Self { - Self::new( - "Stop", - false, - 1, - Some(vec![vec![MidiMessage::Controller { - controller: 123.into(), - value: 0.into() - }]]), - Some(ItemColor::from_tui(Color::Rgb(32, 32, 32)).into()) - ) - } - } - - impl PartialEq for MidiClip { - fn eq (&self, other: &Self) -> bool { - self.uuid == other.uuid - } - } - - impl Eq for MidiClip {} - - impl MidiClip { - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_bool_stub_ (&self) -> bool { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } - fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } - } -} - -mod audio { - use crate::*; - - impl JackPort for AudioInput { - type Port = AudioIn; - type Pair = AudioOut; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec() - }; - port.connect_to_matching()?; - Ok(port) - } - } - - impl JackPort for AudioOutput { - type Port = AudioOut; - type Pair = AudioIn; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec() - }; - port.connect_to_matching()?; - Ok(port) - } - } - - mod meter { - use crate::*; - - impl Draw for RmsMeter { - fn draw(self, to: &mut Tui) -> Usually> { - let XYWH(x, y, w, h) = to.area(); - let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); - } - } - } - } - - impl Draw for Log10Meter { - fn draw(self, to: &mut Tui) -> Usually> { - let XYWH(x, y, w, h) = to.area(); - let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32 / 100.0).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2 - y, Some(Style::default().green())); - } - } - } - } - - fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { - Tui::bg(Black, w_exact(2, iter_east(1, ||meters.iter(), |value, _index|{ - h_full(RmsMeter(*value)) - }))) - } - } -} - -#[cfg(feature = "track")] mod track { - use crate::*; - impl_as_ref!(Vec: |self: App| self.project.as_ref()); - impl_as_mut!(Vec: |self: App| self.project.as_mut()); - #[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt()); - #[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt()); - impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } - impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } } - - impl HasWidth for Track { - const MIN_WIDTH: usize = 9; - fn width_inc (&mut self) { self.width += 1; } - fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } - } - - impl Track { - /// Create a new track with only the default [Sequencer]. - pub fn new ( - name: &impl AsRef, - color: Option, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - ) -> Usually { - Ok(Self { - name: name.as_ref().into(), - color: color.unwrap_or_default(), - sequencer: Sequencer::new(format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to)?, - ..Default::default() - }) - } - pub fn audio_ins (&self) -> &[AudioInput] { - self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() - } - pub fn audio_outs (&self) -> &[AudioOutput] { - self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() - } - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } - pub fn per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + 'a { - iter(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - iter_east(x1 as u16, width, w_exact(width, Tui::fg_bg( - track.color.lightest.rgb, - track.color.base.rgb, - callback(index, track))))}) - } - /// Create a new track connecting the [Sequencer] to a [Sampler]. - #[cfg(feature = "sampler")] pub fn new_with_sampler ( - name: &impl AsRef, - color: Option, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - audio_from: &[&[Connect];2], - audio_to: &[&[Connect];2], - ) -> Usually { - let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; - let client_name = jack.with_client(|c|c.name().to_string()); - let port_name = track.sequencer.midi_outs[0].port_name(); - let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; - track.devices.push(Device::Sampler(Sampler::new( - jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to - )?)); - Ok(track) - } - #[cfg(feature = "sampler")] pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { - for device in self.devices.iter() { - match device { - Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, - _ => {} - } - } - None - } - #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { - for device in self.devices.iter_mut() { - match device { - Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, - _ => {} - } - } - None - } - } - - pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + 'a { - per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track)))) - } - - pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + 'a { - origin_x(Tui::bg(Reset, iter(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - iter_east(x1 as u16, width, w_exact(width, Tui::fg_bg( - track.color.lightest.rgb, - track.color.base.rgb, - callback(index, track))))}))) - } -} - -#[cfg(feature = "scene")] mod scene { - use crate::*; - #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt()); - #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); - #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene()); - #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut()); - impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } - impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } - impl ScenesView for App { - fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(self.w_side()) } - fn w_side (&self) -> u16 { 20 } - fn h_scenes (&self) -> u16 { (self.measure_height() as u16).saturating_sub(20) } - } - impl Scene { - /// 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: &[Track]) -> 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.sequencer().play_clip() { - *clip.read().unwrap() == *c.read().unwrap() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) - } - pub fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } - } - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } - } -} - -#[cfg(feature = "sequencer")] mod sequencer { - use crate::*; - impl_has!(Sequencer: |self: Track| self.sequencer); - impl_has!(Clock: |self: Sequencer| self.clock); - impl_has!(Vec: |self: Sequencer| self.midi_ins); - impl_has!(Vec: |self: Sequencer| self.midi_outs); - impl_has!(Measure: |self: MidiEditor| self.size); - impl_has!(Measure: |self: PianoHorizontal| self.size); - impl_default!(Sequencer: Self { - clock: Clock::default(), - play_clip: None, - next_clip: None, - midi_ins: vec![], - midi_outs: vec![], - recording: false, - monitoring: true, - overdub: false, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - note_buf: vec![0;8], - midi_buf: vec![], - reset: true, - }); - impl Sequencer { - pub fn new ( - name: impl AsRef, - jack: &Jack<'static>, - #[cfg(feature = "clock")] clock: Option<&Clock>, - #[cfg(feature = "clip")] clip: Option<&Arc>>, - #[cfg(feature = "port")] midi_from: &[Connect], - #[cfg(feature = "port")] midi_to: &[Connect], - ) -> Usually { - let _name = name.as_ref(); - #[cfg(feature = "clock")] let clock = clock.cloned().unwrap_or_default(); - Ok(Self { - reset: true, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - #[cfg(feature = "port")] midi_ins: vec![MidiInput::new(jack, &format!("M/{}", name.as_ref()), midi_from)?,], - #[cfg(feature = "port")] midi_outs: vec![MidiOutput::new(jack, &format!("{}/M", name.as_ref()), midi_to)?, ], - #[cfg(feature = "clip")] play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), - #[cfg(feature = "clock")] clock, - ..Default::default() - }) - } - fn process_rolling (&mut self, scope: &ProcessScope) -> Control { - self.process_clear(scope, false); - // Write chunk of clip to output, handle switchover - if self.process_playback(scope) { - self.process_switchover(scope); - } - // Monitor input to output - self.process_monitoring(scope); - // Record and/or monitor input - self.process_recording(scope); - // Emit contents of MIDI buffers to JACK MIDI output ports. - self.midi_outs_emit(scope); - Control::Continue - } - fn process_stopped (&mut self, scope: &ProcessScope) -> Control { - if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { - self.process_monitoring(scope) - } - Control::Continue - } - fn process_monitoring (&mut self, scope: &ProcessScope) { - let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat - let monitoring = self.monitoring(); - for input in self.midi_ins.iter() { - for (sample, event, bytes) in input.parsed(scope) { - if let LiveEvent::Midi { message, .. } = event { - if monitoring { - self.midi_buf[sample].push(bytes.to_vec()); - } - // FIXME: don't lock on every event! - update_keys(&mut notes_in.write().unwrap(), &message); - } - } - } - } - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { - let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); - for frame in &mut self.midi_buf_mut()[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(self.midi_buf_mut()); - } - for port in self.midi_outs_mut().iter_mut() { - // Clear output buffer(s) - port.buffer_clear(scope, false); - } - } - fn process_recording (&mut self, scope: &ProcessScope) { - if self.monitoring() { - self.monitor(scope); - } - if let Some((started, ref clip)) = self.play_clip.clone() { - self.record_clip(scope, started, clip); - } - if let Some((_start_at, _clip)) = &self.next_clip() { - self.record_next(); - } - } - fn process_playback (&mut self, scope: &ProcessScope) -> bool { - // If a clip is playing, write a chunk of MIDI events from it to the output buffer. - // If no clip is playing, prepare for switchover immediately. - if let Some((started, clip)) = &self.play_clip { - // Length of clip, to repeat or stop on end. - let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); - // Index of first sample to populate. - let offset = self.clock().get_sample_offset(scope, &started); - // Write MIDI events from clip at sample offsets corresponding to pulses. - for (sample, pulse) in self.clock().get_pulses(scope, offset) { - // If a next clip is enqueued, and we're past the end of the current one, - // break the loop here (FIXME count pulse correctly) - let past_end = if clip.is_some() { pulse >= length } else { true }; - // Is it time for switchover? - if self.next_clip().is_some() && past_end { - return true - } - // If there's a currently playing clip, output notes from it to buffer: - if let Some(clip) = clip { - // Source clip from which the MIDI events will be taken. - let clip = clip.read().unwrap(); - // Clip with zero length is not processed - if clip.length > 0 { - // Current pulse index in source clip - let pulse = pulse % clip.length; - // Output each MIDI event from clip at appropriate frames of output buffer: - for message in clip.notes[pulse].iter() { - for port in self.midi_outs.iter_mut() { - port.buffer_write(sample, LiveEvent::Midi { - channel: 0.into(), /* TODO */ - message: *message - }); - } - } - } - } - } - false - } else { - true - } - } - /// Handle switchover from current to next playing clip. - fn process_switchover (&mut self, scope: &ProcessScope) { - let _midi_buf = self.midi_buf_mut(); - let sample0 = scope.last_frame_time() as usize; - //let samples = scope.n_frames() as usize; - if let Some((start_at, clip)) = &self.next_clip() { - let start = start_at.sample.get() as usize; - let sample = self.clock().started.read().unwrap() - .as_ref().unwrap().sample.get() as usize; - // If it's time to switch to the next clip: - if start <= sample0.saturating_sub(sample) { - // Samples elapsed since clip was supposed to start - let _skipped = sample0 - start; - // Switch over to enqueued clip - let started = Moment::from_sample(self.clock().timebase(), start as f64); - // Launch enqueued clip - *self.play_clip_mut() = Some((started, clip.clone())); - // Unset enqueuement (TODO: where to implement looping?) - *self.next_clip_mut() = None; - // Fill in remaining ticks of chunk from next clip. - self.process_playback(scope); - } - } - } - } - impl HasMidiBuffers for Sequencer { - fn note_buf_mut (&mut self) -> &mut Vec { &mut self.note_buf } - fn midi_buf_mut (&mut self) -> &mut Vec>> { &mut self.midi_buf } - } - impl std::fmt::Debug for Sequencer { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Sequencer") - .field("clock", &self.clock) - .field("play_clip", &self.play_clip) - .field("next_clip", &self.next_clip) - .finish() - } - } - impl MidiMonitor for Sequencer { - fn monitoring (&self) -> bool { self.monitoring } - fn monitoring_mut (&mut self) -> &mut bool { &mut self.monitoring } - fn notes_in (&self) -> &Arc> { &self.notes_in } - } - impl MidiRecord for Sequencer { - fn recording (&self) -> bool { self.recording } - fn recording_mut (&mut self) -> &mut bool { &mut self.recording } - fn overdub (&self) -> bool { self.overdub } - fn overdub_mut (&mut self) -> &mut bool { &mut self.overdub } - } - #[cfg(feature="clip")] impl HasPlayClip for Sequencer { - fn reset (&self) -> bool { self.reset } - fn reset_mut (&mut self) -> &mut bool { &mut self.reset } - fn play_clip (&self) -> &Option<(Moment, Option>>)> { - &self.play_clip - } - fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.play_clip - } - fn next_clip (&self) -> &Option<(Moment, Option>>)> { - &self.next_clip - } - fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.next_clip - } - } - /// JACK process callback for a sequencer's clip sequencer/recorder. - impl Audio for Sequencer { - fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - if self.clock().is_rolling() { - self.process_rolling(scope) - } else { - self.process_stopped(scope) - } - } - } - impl Draw for MidiEditor { - fn draw(self, to: &mut Tui) -> Usually> { - self.tui().draw(to) - } - } - impl Draw for PianoHorizontal { - fn draw(self, to: &mut Tui) -> Usually> { - self.tui().draw(to) - } - } -} - -#[cfg(feature = "editor")] mod editor { - use crate::*; - impl std::fmt::Debug for MidiEditor { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("MidiEditor").field("mode", &self.mode).finish() - } - } - impl_from!(MidiEditor: |clip: &Arc>| { - let model = Self::from(Some(clip.clone())); - model.redraw(); - model - }); - impl_from!(MidiEditor: |clip: Option>>| { - let mut model = Self::default(); - *model.clip_mut() = clip; - model.redraw(); - model - }); - impl_default!(MidiEditor: Self { - size: Measure::new(0, 0), mode: PianoHorizontal::new(None) - }); - impl_default!(OctaveVertical: Self { - on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] - }); - impl MidiEditor { - /// Put note at current position - pub fn put_note (&mut self, advance: bool) { - let mut redraw = false; - if let Some(clip) = self.clip() { - let mut clip = clip.write().unwrap(); - let note_start = self.get_time_pos(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let note_end = note_start + (note_len.saturating_sub(1)); - let key: u7 = u7::from(note_pos as u8); - let vel: u7 = 100.into(); - let length = clip.length; - let note_end = note_end % length; - let note_on = MidiMessage::NoteOn { key, vel }; - if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { - clip.notes[note_start].push(note_on); - } - let note_off = MidiMessage::NoteOff { key, vel }; - if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { - clip.notes[note_end].push(note_off); - } - if advance { - self.set_time_pos((note_end + 1) % clip.length); - } - redraw = true; - } - if redraw { - self.mode.redraw(); - } - } - fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } - fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } - fn note_length (&self) -> usize { self.get_note_len() } - fn note_pos (&self) -> usize { self.get_note_pos() } - fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } - fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } - fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } - fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } - fn note_len (&self) -> usize { self.get_note_len() } - fn note_len_next (&self) -> usize { self.get_note_len() + 1 } - fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } - fn note_range (&self) -> usize { self.get_note_axis() } - fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } - fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } - fn time_zoom (&self) -> usize { self.get_time_zoom() } - fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_lock (&self) -> bool { self.get_time_lock() } - fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } - fn time_pos (&self) -> usize { self.get_time_pos() } - fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } - fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } - fn time_pos_prev (&self) -> usize { - let step = self.get_note_len(); - self.get_time_pos().overflowing_sub(step) - .0.min(self.clip_length().saturating_sub(step)) - } - fn time_pos_prev_fine (&self) -> usize { - self.get_time_pos().overflowing_sub(1) - .0.min(self.clip_length().saturating_sub(1)) - } - pub fn clip_status (&self) -> impl Draw + '_ { - let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.name.clone(), clip.length, clip.looped) - } else { (ItemTheme::G[64], String::new().into(), 0, false) }; - w_exact(20, south!( - w_full(origin_w(east( - button_2("f2", "name ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), - w_full(origin_w(east( - button_2("l", "ength ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), - w_full(origin_w(east( - button_2("r", "epeat ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), - )) - } - pub fn edit_status (&self) -> impl Draw + '_ { - let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.length) - } else { (ItemTheme::G[64], 0) }; - let time_pos = self.get_time_pos(); - let time_zoom = self.get_time_zoom(); - let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; - let note_pos = self.get_note_pos(); - let note_name = format!("{:4}", note_pitch_to_name(note_pos)); - let note_pos = format!("{:>3}", note_pos); - let note_len = format!("{:>4}", self.get_note_len()); - w_exact(20, south!( - w_full(origin_w(east( - button_2("t", "ime ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), - format!("{length} /{time_zoom} +{time_pos} "))))))), - w_full(origin_w(east( - button_2("z", "lock ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), - format!("{time_lock}"))))))), - w_full(origin_w(east( - button_2("x", "note ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), - format!("{note_name} {note_pos} {note_len}"))))))), - )) - } - } - - impl TimeRange for MidiEditor { - fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } - } - - impl NoteRange for MidiEditor { - fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } - } - - impl NotePoint for MidiEditor { - fn note_len (&self) -> &AtomicUsize { self.mode.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } - } - - impl TimePoint for MidiEditor { - fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } - } - - impl MidiViewer for MidiEditor { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } - fn redraw (&self) { self.mode.redraw() } - fn clip (&self) -> &Option>> { self.mode.clip() } - fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } - fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } - } - - impl MidiEditor { - fn tui (&self) -> impl Draw { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } - } - - - impl PianoHorizontal { - pub fn new (clip: Option<&Arc>>) -> Self { - let size = Measure::new(0, 0); - let mut range = MidiSelection::from((12, true)); - range.time_axis = size.x.clone(); - range.note_axis = size.y.clone(); - let piano = Self { - keys_width: 5, - size, - range, - buffer: RwLock::new(Default::default()).into(), - point: MidiCursor::default(), - clip: clip.cloned(), - color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), - }; - piano.redraw(); - piano - } - } - - impl PianoHorizontal { - fn tui (&self) -> impl Draw { - south( - east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), - east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(self.cursor())))), - ) - } - } - - impl PianoHorizontal { - /// Draw the piano roll background. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { - for (y, note) in (0..=127).rev().enumerate() { - for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { - let cell = buf.get_mut(x, y).unwrap(); - if note == (127-note_point) || time == time_point { - cell.set_bg(Rgb(0,0,0)); - } else { - cell.set_bg(clip.color.darkest.rgb); - } - if time % 384 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('│'); - } else if time % 96 == 0 { - cell.set_fg(clip.color.dark.rgb); - cell.set_char('╎'); - } else if time % note_len == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('┊'); - } else if (127 - note) % 12 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('='); - } else if (127 - note) % 6 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('—'); - } else { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('·'); - } - } - } - } - /// Draw the piano roll foreground. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { - let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); - let mut notes_on = [false;128]; - for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { - for (_y, note) in (0..=127).rev().enumerate() { - if let Some(cell) = buf.get_mut(x, note) { - if notes_on[note] { - cell.set_char('▂'); - cell.set_style(style); - } - } - } - let time_end = time_start + zoom; - for time in time_start..time_end.min(clip.length) { - for event in clip.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - if let Some(cell) = buf.get_mut(x, note) { - cell.set_char('█'); - cell.set_style(style); - } - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } - } - } - - } - } - fn notes (&self) -> impl Draw { - let time_start = self.get_time_start(); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - let buffer = self.buffer.clone(); - Thunk::new(move|to: &mut Tui|{ - let source = buffer.read().unwrap(); - let XYWH(x0, y0, w, _h) = to.area(); - //if h as usize != note_axis { - //panic!("area height mismatch: {h} <> {note_axis}"); - //} - for (area_x, screen_x) in (x0..x0+w).enumerate() { - for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { - let source_x = time_start + area_x; - let source_y = note_hi - area_y; - // TODO: enable loop rollover: - //let source_x = (time_start + area_x) % source.width.max(1); - //let source_y = (note_hi - area_y) % source.height.max(1); - let is_in_x = source_x < source.width; - let is_in_y = source_y < source.height; - if is_in_x && is_in_y { - if let Some(source_cell) = source.get(source_x, source_y) { - if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { - *cell = source_cell.clone(); - } - } - } - } - } - }) - } - fn cursor (&self) -> impl Draw { - let note_hi = self.get_note_hi(); - let note_lo = self.get_note_lo(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let time_pos = self.get_time_pos(); - let time_start = self.get_time_start(); - let time_zoom = self.get_time_zoom(); - let style = Some(Style::default().fg(self.color.lightest.rgb)); - Thunk::new(move|to: &mut Tui|{ - let XYWH(x0, y0, w, _) = to.area(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - if note == note_pos { - for x in 0..w { - let screen_x = x0 + x; - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_pos && time_pos < time_2 { - to.blit(&"█", screen_x, screen_y, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (screen_x + 1)..(screen_x + tail) { - to.blit(&"▂", x_tail, screen_y, style); - } - break - } - } - break - } - } - }) - } - fn keys (&self) -> impl Draw { - let state = self; - let color = state.color; - let note_lo = state.get_note_lo(); - let note_hi = state.get_note_hi(); - let note_pos = state.get_note_pos(); - let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); - let off_style = Some(Style::default().fg(Tui::g(255))); - let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); - h_full(w_exact(self.keys_width, Thunk::new(move|to: &mut Tui|{ - let XYWH(x, y0, _w, _h) = to.area(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - to.blit(&to_key(note), x, screen_y, key_style); - if note > 127 { - continue - } - if note == note_pos { - to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style) - } else { - to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) - }; - } - }))) - } - fn timeline (&self) -> impl Draw + '_ { - w_full(h_exact(1, Thunk::new(move|to: &mut Tui|{ - let XYWH(x, y, w, _h) = to.area(); - let style = Some(Style::default().dim()); - let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { - let t = area_x as usize * self.time_zoom().get(); - if t < length { - to.blit(&"|", screen_x, y, style); - } - } - }))) - } - } - - impl TimeRange for PianoHorizontal { - fn time_len (&self) -> &AtomicUsize { self.range.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.range.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } - } - - impl NoteRange for PianoHorizontal { - fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } - } - - impl NotePoint for PianoHorizontal { - fn note_len (&self) -> &AtomicUsize { self.point.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } - } - - impl TimePoint for PianoHorizontal { - fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } - } - - impl MidiViewer for PianoHorizontal { - fn clip (&self) -> &Option>> { &self.clip } - fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } - /// Determine the required space to render the clip. - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { (clip.length / self.range.time_zoom().get(), 128) } - fn redraw(self) { - *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { - let clip = clip.read().unwrap(); - let buf_size = self.buffer_size(&clip); - let mut buffer = BigBuffer::from(buf_size); - let time_zoom = self.get_time_zoom(); - self.time_len().set(clip.length); - PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); - PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); - buffer - } else { - Default::default() - } - } - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); - self.redraw(); - } - } - - impl std::fmt::Debug for PianoHorizontal { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - let buffer = self.buffer.read().unwrap(); - f.debug_struct("PianoHorizontal") - .field("time_zoom", &self.range.time_zoom) - .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) - .finish() - } - } - impl OctaveVertical { - fn color (&self, pitch: usize) -> Color { - let pitch = pitch % 12; - self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] - } - } - impl OctaveVertical { - fn tui (&self) -> impl Draw { - east!( - Tui::fg_bg(self.color(0), self.color(1), "▙"), - Tui::fg_bg(self.color(2), self.color(3), "▙"), - Tui::fg_bg(self.color(4), self.color(5), "▌"), - Tui::fg_bg(self.color(6), self.color(7), "▟"), - Tui::fg_bg(self.color(8), self.color(9), "▟"), - Tui::fg_bg(self.color(10), self.color(11), "▟"), - ) - } - } -} - -#[cfg(feature = "sampler")] mod sampler { - use crate::*; - impl Default for SampleKit { - fn default () -> Self { Self([const { None }; N]) } - } - impl Iterator for Voice { - type Item = [f32;2]; - fn next (&mut self) -> Option { - if self.after > 0 { - self.after -= 1; - return Some([0.0, 0.0]) - } - let sample = self.sample.read().unwrap(); - if self.position < sample.end { - let position = self.position; - self.position += 1; - return sample.channels[0].get(position).map(|_amplitude|[ - sample.channels[0][position] * self.velocity * sample.gain, - sample.channels[0][position] * self.velocity * sample.gain, - ]) - } - None - } - } - impl NoteRange for Sampler { - fn note_lo (&self) -> &AtomicUsize { - &self.note_lo - } - fn note_axis (&self) -> &AtomicUsize { - &self.size.y - } - } - impl NotePoint for Sampler { - fn note_len (&self) -> &AtomicUsize { - unreachable!(); - } - fn get_note_len (&self) -> usize { - 0 - } - fn set_note_len (&self, _x: usize) -> usize { - 0 /*TODO?*/ - } - fn note_pos (&self) -> &AtomicUsize { - &self.note_pt - } - fn get_note_pos (&self) -> usize { - self.note_pt.load(Relaxed) - } - fn set_note_pos (&self, x: usize) -> usize { - let old = self.note_pt.swap(x, Relaxed); - self.cursor.0.store(x % 8, Relaxed); - self.cursor.1.store(x / 8, Relaxed); - old - } - } - impl Sampler { - pub fn new ( - jack: &Jack<'static>, - name: impl AsRef, - #[cfg(feature = "port")] midi_from: &[Connect], - #[cfg(feature = "port")] audio_from: &[&[Connect];2], - #[cfg(feature = "port")] audio_to: &[&[Connect];2], - ) -> Usually { - let name = name.as_ref(); - Ok(Self { - name: name.into(), - input_meters: vec![0.0;2], - output_meters: vec![0.0;2], - output_gain: 1., - buffer: vec![vec![0.0;16384];2], - #[cfg(feature = "port")] midi_in: Some( - MidiInput::new(jack, &format!("M/{name}"), midi_from)? - ), - #[cfg(feature = "port")] audio_ins: vec![ - AudioInput::new(jack, &format!("L/{name}"), audio_from[0])?, - AudioInput::new(jack, &format!("R/{name}"), audio_from[1])?, - ], - #[cfg(feature = "port")] audio_outs: vec![ - AudioOutput::new(jack, &format!("{name}/L"), audio_to[0])?, - AudioOutput::new(jack, &format!("{name}/R"), audio_to[1])?, - ], - ..Default::default() - }) - } - /// Value of cursor - pub fn cursor (&self) -> (usize, usize) { - (self.cursor.0.load(Relaxed), self.cursor.1.load(Relaxed)) - } - fn sample_selected (&self) -> usize { - (self.get_note_pos() as u8).into() - } - fn sample_selected_pitch (&self) -> u7 { - (self.get_note_pos() as u8).into() - } - pub fn process_audio_in (&mut self, scope: &ProcessScope) { - self.reset_input_meters(); - if self.recording.is_some() { - self.record_into(scope); - } else { - self.update_input_meters(scope); - } - } - /// Make sure that input meter count corresponds to input channel count - fn reset_input_meters (&mut self) { - let channels = self.audio_ins.len(); - if self.input_meters.len() != channels { - self.input_meters = vec![f32::MIN;channels]; - } - } - /// Record from inputs to sample - fn record_into (&mut self, scope: &ProcessScope) { - if let Some(ref sample) = self.recording.as_ref().expect("no recording sample").1 { - let mut sample = sample.write().unwrap(); - if sample.channels.len() != self.audio_ins.len() { - panic!("channel count mismatch"); - } - let samples_with_meters = self.audio_ins.iter() - .zip(self.input_meters.iter_mut()) - .zip(sample.channels.iter_mut()); - let mut length = 0; - for ((input, meter), channel) in samples_with_meters { - let slice = input.port().as_slice(scope); - length = length.max(slice.len()); - *meter = to_rms(slice); - channel.extend_from_slice(slice); - } - sample.end += length; - } else { - panic!("tried to record into the void") - } - } - /// Update input meters - fn update_input_meters (&mut self, scope: &ProcessScope) { - for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { - let slice = input.port().as_slice(scope); - *meter = to_rms(slice); - } - } - /// Make sure that output meter count corresponds to input channel count - fn reset_output_meters (&mut self) { - let channels = self.audio_outs.len(); - if self.output_meters.len() != channels { - self.output_meters = vec![f32::MIN;channels]; - } - } - /// Mix all currently playing samples into the output. - pub fn process_audio_out (&mut self, scope: &ProcessScope) { - self.clear_output_buffer(); - self.populate_output_buffer(scope.n_frames() as usize); - self.write_output_buffer(scope); - } - /// Zero the output buffer. - fn clear_output_buffer (&mut self) { - for buffer in self.buffer.iter_mut() { - buffer.fill(0.0); - } - } - /// Write playing voices to output buffer - fn populate_output_buffer (&mut self, frames: usize) { - let Sampler { buffer, voices, output_gain, mixing_mode, .. } = self; - let _channel_count = buffer.len(); - match mixing_mode { - MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{ - mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) - }), - MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{ - mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) - }), - } - } - /// Write output buffer to output ports. - fn write_output_buffer (&mut self, scope: &ProcessScope) { - let Sampler { audio_outs, buffer, .. } = self; - for (i, port) in audio_outs.iter_mut().enumerate() { - let buffer = &buffer[i]; - for (i, value) in port.port_mut().as_mut_slice(scope).iter_mut().enumerate() { - *value = *buffer.get(i).unwrap_or(&0.0); - } - } - } - } - impl SampleAdd { - fn exited (&self) -> bool { - self.exited - } - fn exit (&mut self) { - self.exited = true - } - pub fn new ( - sample: &Arc>, - voices: &Arc>> - ) -> Usually { - let dir = std::env::current_dir()?; - let (subdirs, files) = scan(&dir)?; - Ok(Self { - exited: false, - dir, - subdirs, - files, - cursor: 0, - offset: 0, - sample: sample.clone(), - voices: voices.clone(), - _search: None - }) - } - fn rescan (&mut self) -> Usually<()> { - scan(&self.dir).map(|(subdirs, files)|{ - self.subdirs = subdirs; - self.files = files; - }) - } - fn prev (&mut self) { - self.cursor = self.cursor.saturating_sub(1); - } - fn next (&mut self) { - self.cursor = self.cursor + 1; - } - fn try_preview (&mut self) -> Usually<()> { - if let Some(path) = self.cursor_file() { - if let Ok(sample) = Sample::from_file(&path) { - *self.sample.write().unwrap() = sample; - self.voices.write().unwrap().push( - Sample::play(&self.sample, 0, &u7::from(100u8)) - ); - } - //load_sample(&path)?; - //let src = std::fs::File::open(&path)?; - //let mss = MediaSourceStream::new(Box::new(src), Default::default()); - //let mut hint = Hint::new(); - //if let Some(ext) = path.extension() { - //hint.with_extension(&ext.to_string_lossy()); - //} - //let meta_opts: MetadataOptions = Default::default(); - //let fmt_opts: FormatOptions = Default::default(); - //if let Ok(mut probed) = symphonia::default::get_probe() - //.format(&hint, mss, &fmt_opts, &meta_opts) - //{ - //panic!("{:?}", probed.format.metadata()); - //}; - } - Ok(()) - } - fn cursor_dir (&self) -> Option { - if self.cursor < self.subdirs.len() { - Some(self.dir.join(&self.subdirs[self.cursor])) - } else { - None - } - } - fn cursor_file (&self) -> Option { - if self.cursor < self.subdirs.len() { - return None - } - let index = self.cursor.saturating_sub(self.subdirs.len()); - if index < self.files.len() { - Some(self.dir.join(&self.files[index])) - } else { - None - } - } - fn pick (&mut self) -> Usually { - if self.cursor == 0 { - if let Some(parent) = self.dir.parent() { - self.dir = parent.into(); - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - } - if let Some(dir) = self.cursor_dir() { - self.dir = dir; - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - if let Some(path) = self.cursor_file() { - let (end, channels) = read_sample_data(&path.to_string_lossy())?; - let mut sample = self.sample.write().unwrap(); - sample.name = path.file_name().unwrap().to_string_lossy().into(); - sample.end = end; - sample.channels = channels; - return Ok(true) - } - return Ok(false) - } - } - impl SampleKit { - pub fn get (&self, index: usize) -> &Option>> { - if index < self.0.len() { - &self.0[index] - } else { - &None - } - } - } - impl Sample { - pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { - Self { - name: name.as_ref().into(), - start, - end, - channels, - rate: None, - gain: 1.0, - color: ItemTheme::random(), - } - } - pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { - Voice { - sample: sample.clone(), - after, - position: sample.read().unwrap().start, - velocity: velocity.as_int() as f32 / 127.0, - } - } - pub fn handle_cc (&mut self, controller: u7, value: u7) { - let percentage = value.as_int() as f64 / 127.; - match controller.as_int() { - 20 => { - self.start = (percentage * self.end as f64) as usize; - }, - 21 => { - let length = self.channels[0].len(); - self.end = length.min( - self.start + (percentage * (length as f64 - self.start as f64)) as usize - ); - }, - 22 => { /*attack*/ }, - 23 => { /*decay*/ }, - 24 => { - self.gain = percentage as f32 * 2.0; - }, - 26 => { /* pan */ } - 25 => { /* pitch */ } - _ => {} - } - } - /// Read WAV from file - pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { - let mut channels: Vec> = vec![]; - for channel in wavers::Wav::from_path(src)?.channels() { - channels.push(channel); - } - let mut end = 0; - let mut data: Vec> = vec![]; - for samples in channels.iter() { - let channel = Vec::from(samples.as_ref()); - end = end.max(channel.len()); - data.push(channel); - } - Ok((end, data)) - } - pub fn from_file (path: &PathBuf) -> Usually { - let name = path.file_name().unwrap().to_string_lossy().into(); - let mut sample = Self { name, ..Default::default() }; - // Use file extension if present - let mut hint = Hint::new(); - if let Some(ext) = path.extension() { - hint.with_extension(&ext.to_string_lossy()); - } - let probed = symphonia::default::get_probe().format( - &hint, - MediaSourceStream::new( - Box::new(File::open(path)?), - Default::default(), - ), - &Default::default(), - &Default::default() - )?; - let mut format = probed.format; - let params = &format.tracks().iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .expect("no tracks found") - .codec_params; - let mut decoder = get_codecs().make(params, &Default::default())?; - loop { - match format.next_packet() { - Ok(packet) => sample.decode_packet(&mut decoder, packet)?, - Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), - Err(err) => return Err(err.into()), - }; - }; - sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); - Ok(sample) - } - fn decode_packet ( - &mut self, decoder: &mut Box, packet: Packet - ) -> Usually<()> { - // Decode a packet - let decoded = decoder - .decode(&packet) - .map_err(|e|Box::::from(e))?; - // Determine sample rate - let spec = *decoded.spec(); - if let Some(rate) = self.rate { - if rate != spec.rate as usize { - panic!("sample rate changed"); - } - } else { - self.rate = Some(spec.rate as usize); - } - // Determine channel count - while self.channels.len() < spec.channels.count() { - self.channels.push(vec![]); - } - // Load sample - let mut samples = SampleBuffer::new( - decoded.frames() as u64, - spec - ); - if samples.capacity() > 0 { - samples.copy_interleaved_ref(decoded); - for frame in samples.samples().chunks(spec.channels.count()) { - for (chan, frame) in frame.iter().enumerate() { - self.channels[chan].push(*frame) - } - } - } - Ok(()) - } - } - impl Draw for SampleAdd { - fn draw (self, _to: &mut Tui) -> Usually> { - todo!() - } - } - - fn draw_list_item (sample: &Option>>) -> String { - if let Some(sample) = sample { - let sample = sample.read().unwrap(); - format!("{:8}", sample.name) - //format!("{:8} {:3} {:6}-{:6}/{:6}", - //sample.name, - //sample.gain, - //sample.start, - //sample.end, - //sample.channels[0].len() - //) - } else { - String::from("........") - } - } - - fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + use<'_> { - let min_db = -64.0; - Thunk::new(move|to: &mut Tui|{ - let XYWH(x, y, width, height) = to.area(); - let area = Rect { x, y, width, height }; - if let Some(sample) = &sample { - let sample = sample.read().unwrap(); - let start = sample.start as f64; - let end = sample.end as f64; - let length = end - start; - let step = length / width as f64; - let mut t = start; - let mut lines = vec![]; - while t < end { - let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; - let total: f32 = chunk.iter().map(|x|x.abs()).sum(); - let count = chunk.len() as f32; - let meter = 10. * (total / count).log10(); - let x = t as f64; - let y = meter as f64; - lines.push(Line::new(x, min_db, x, y, Color::Green)); - t += step / 2.; - } - Canvas::default() - .x_bounds([sample.start as f64, sample.end as f64]) - .y_bounds([min_db, 0.]) - .paint(|ctx| { - for line in lines.iter() { - ctx.draw(line); - } - //FIXME: proportions - //let text = "press record to finish sampling"; - //ctx.print( - //(width - text.len() as u16) as f64 / 2.0, - //height as f64 / 2.0, - //text.red() - //); - }).render(area, &mut to.buffer); - } else { - Canvas::default() - .x_bounds([0.0, width as f64]) - .y_bounds([0.0, height as f64]) - .paint(|_ctx| { - //let text = "press record to begin sampling"; - //ctx.print( - //(width - text.len() as u16) as f64 / 2.0, - //height as f64 / 2.0, - //text.red() - //); - }) - .render(area, &mut to.buffer); - } - }) - } - - impl_audio!(Sampler: sampler_jack_process); - pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control { - if let Some(midi_in) = &state.midi_in { - for midi in midi_in.port().iter(scope) { - sampler_midi_in(&state.samples, &state.voices, midi) - } - } - state.process_audio_out(scope); - state.process_audio_in(scope); - Control::Continue - } - - /// Create [Voice]s from [Sample]s in response to MIDI input. - fn sampler_midi_in ( - samples: &SampleKit<128>, voices: &Arc>>, RawMidi { time, bytes }: RawMidi - ) { - if let Ok(LiveEvent::Midi { message, .. }) = LiveEvent::parse(bytes) { - match message { - MidiMessage::NoteOn { ref key, ref vel } => { - if let Some(sample) = samples.get(key.as_int() as usize) { - voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); - } - }, - MidiMessage::Controller { controller: _, value: _ } => { - // TODO - } - _ => {} - } - } - } - - fn draw_sample ( - to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool - ) -> Usually { - let style = if focus { Style::default().green() } else { Style::default() }; - if focus { - to.blit(&"🬴", x+1, y, Some(style.bold())); - } - let label1 = format!("{:3} {:12}", - note.map(|n|n.to_string()).unwrap_or(String::default()), - sample.name); - let label2 = format!("{:>6} {:>6} +0.0", - sample.start, - sample.end); - to.blit(&label1, x+2, y, Some(style.bold())); - to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); - Ok(label1.len() + label2.len() + 4) - } -} - -#[cfg(feature = "lv2")] mod lv2 { - use crate::*; - impl_audio!(Lv2: lv2_jack_process); - impl Lv2 { - const INPUT_BUFFER: usize = 1024; - pub fn new ( - jack: &Jack<'static>, - name: &str, - uri: &str, - ) -> Usually { - let lv2_world = livi::World::with_load_bundle(&uri); - let lv2_features = lv2_world.build_features(livi::FeaturesBuilder { - min_block_length: 1, - max_block_length: 65536, - }); - let lv2_plugin = lv2_world.iter_plugins().nth(0) - .unwrap_or_else(||panic!("plugin not found: {uri}")); - Ok(Self { - jack: jack.clone(), - name: name.into(), - path: Some(String::from(uri).into()), - selected: 0, - mapping: false, - midi_ins: vec![], - midi_outs: vec![], - audio_ins: vec![], - audio_outs: vec![], - lv2_instance: unsafe { - lv2_plugin - .instantiate(lv2_features.clone(), 48000.0) - .expect(&format!("instantiate failed: {uri}")) - }, - lv2_port_list: lv2_plugin.ports().collect::>(), - lv2_input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), - lv2_ui_thread: None, - lv2_world, - lv2_features, - lv2_plugin, - }) - } - } - fn lv2_jack_process ( - Lv2 { - midi_ins, midi_outs, audio_ins, audio_outs, - lv2_features, lv2_instance, lv2_input_buffer, .. - }: &mut Lv2, - _client: &Client, - scope: &ProcessScope - ) -> Control { - let urid = lv2_features.midi_urid(); - lv2_input_buffer.clear(); - for port in midi_ins.iter() { - let mut atom = ::livi::event::LV2AtomSequence::new( - &lv2_features, - scope.n_frames() as usize - ); - for event in port.iter(scope) { - match event.bytes.len() { - 3 => atom.push_midi_event::<3>( - event.time as i64, - urid, - &event.bytes[0..3] - ).unwrap(), - _ => {} - } - } - lv2_input_buffer.push(atom); - } - let mut outputs = vec![]; - for _ in midi_outs.iter() { - outputs.push(::livi::event::LV2AtomSequence::new( - lv2_features, - scope.n_frames() as usize - )); - } - let ports = ::livi::EmptyPortConnections::new() - .with_atom_sequence_inputs(lv2_input_buffer.iter()) - .with_atom_sequence_outputs(outputs.iter_mut()) - .with_audio_inputs(audio_ins.iter().map(|o|o.as_slice(scope))) - .with_audio_outputs(audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); - unsafe { - lv2_instance.run(scope.n_frames() as usize, ports).unwrap() - }; - Control::Continue - } - - impl LV2PluginUI { pub fn new () -> Usually { Ok(Self { window: None }) } } - - impl ApplicationHandler for LV2PluginUI { - fn resumed (&mut self, event_loop: &ActiveEventLoop) { - self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); - } - fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { - match event { - WindowEvent::CloseRequested => { - self.window.as_ref().unwrap().set_visible(false); - event_loop.exit(); - }, - WindowEvent::RedrawRequested => { - self.window.as_ref().unwrap().request_redraw(); - } - _ => (), - } - } - } - - impl Draw for Lv2 { - fn draw(self, to: &mut Tui) { - let area = to.area(); - let XYWH(x, y, _, height) = area; - let mut width = 20u16; - let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1)); - let end = start + height as usize - 2; - //draw_box(buf, Rect { x, y, width, height }); - for i in start..end { - if let Some(port) = self.lv2_port_list.get(i) { - let value = if let Some(value) = self.lv2_instance.control_input(port.index) { - value - } else { - port.default_value - }; - //let label = &format!("C·· M·· {:25} = {value:.03}", port.name); - let label = &format!("{:25} = {value:.03}", port.name); - width = width.max(label.len() as u16 + 4); - let style = if i == self.selected { - Some(Style::default().green()) - } else { - None - } ; - to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style); - } else { - break - } - } - draw_header(self, to, x, y, width); - } - } - - - fn draw_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) { - let style = Style::default().gray(); - let label1 = format!(" {}", state.name); - to.blit(&label1, x + 1, y, Some(style.white().bold())); - if let Some(ref path) = state.path { - let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]); - to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim())); - } - //Ok(Rect { x, y, width: w, height: 1 }) - } -} - -mod pool { - use crate::*; - has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); - impl_has_clips!(|self: Pool|self.clips); - impl_from!(Pool: |clip:&Arc>|{ - let model = Self::default(); - model.clips.write().unwrap().push(clip.clone()); - model.clip.store(1, Relaxed); - model - }); - impl_default!(Pool: Self { - browse: None, - clip: 0.into(), - clips: Arc::from(RwLock::from(vec![])), - mode: None, - samples: Arc::from(RwLock::from(vec![])), - visible: true, - }); - impl Pool { - pub fn clip_index (&self) -> usize { - self.clip.load(Relaxed) - } - pub fn set_clip_index (&self, value: usize) { - self.clip.store(value, Relaxed); - } - pub fn mode (&self) -> &Option { - &self.mode - } - pub fn mode_mut (&mut self) -> &mut Option { - &mut self.mode - } - pub fn begin_clip_length (&mut self) { - let length = self.clips()[self.clip_index()].read().unwrap().length; - *self.mode_mut() = Some(PoolMode::Length( - self.clip_index(), - length, - ClipLengthFocus::Bar - )); - } - pub fn begin_clip_rename (&mut self) { - let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); - *self.mode_mut() = Some(PoolMode::Rename( - self.clip_index(), - name - )); - } - pub fn begin_import (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Import( - self.clip_index(), - Browse::new(None)? - )); - Ok(()) - } - pub fn begin_export (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Export( - self.clip_index(), - Browse::new(None)? - )); - Ok(()) - } - pub fn new_clip (&self) -> MidiClip { - MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) - } - pub fn cloned_clip (&self) -> MidiClip { - let index = self.clip_index(); - let mut clip = self.clips()[index].read().unwrap().duplicate(); - clip.color = ItemTheme::random_near(clip.color, 0.25); - clip - } - pub fn add_new_clip (&self) -> (usize, Arc>) { - let clip = Arc::new(RwLock::new(self.new_clip())); - let index = { - let mut clips = self.clips.write().unwrap(); - clips.push(clip.clone()); - clips.len().saturating_sub(1) - }; - self.clip.store(index, Relaxed); - (index, clip) - } - pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { - let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); - if let Some(index) = index { - self.clips.write().unwrap().remove(index); - return true - } - false - } - } - impl ClipLengthFocus { - pub fn next (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } - } - pub fn prev (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } - } - } - impl ClipLength { - pub fn _new (pulses: usize, focus: Option) -> Self { - Self { ppq: PPQ, bpb: 4, pulses, focus } - } - pub fn bars (&self) -> usize { - self.pulses / (self.bpb * self.ppq) - } - pub fn beats (&self) -> usize { - (self.pulses % (self.bpb * self.ppq)) / self.ppq - } - pub fn ticks (&self) -> usize { - self.pulses % self.ppq - } - pub fn bars_string (&self) -> Arc { - format!("{}", self.bars()).into() - } - pub fn beats_string (&self) -> Arc { - format!("{}", self.beats()).into() - } - pub fn ticks_string (&self) -> Arc { - format!("{:>02}", self.ticks()).into() - } - } - - impl Pool { - fn _todo_usize_ (&self) -> usize { todo!() } - fn _todo_bool_ (&self) -> bool { todo!() } - fn _todo_clip_ (&self) -> MidiClip { todo!() } - fn _todo_path_ (&self) -> PathBuf { todo!() } - fn _todo_color_ (&self) -> ItemColor { todo!() } - fn _todo_str_ (&self) -> Arc { todo!() } - fn _clip_new (&self) -> MidiClip { self.new_clip() } - fn _clip_cloned (&self) -> MidiClip { self.cloned_clip() } - fn _clip_index_current (&self) -> usize { 0 } - fn _clip_index_after (&self) -> usize { 0 } - fn _clip_index_previous (&self) -> usize { 0 } - fn _clip_index_next (&self) -> usize { 0 } - fn _color_random (&self) -> ItemColor { ItemColor::random() } - } - - impl<'a> PoolView<'a> { - fn tui (&self) -> impl Draw { - let Self(pool) = self; - //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); - //let on_bg = |x|x;//below(Repeat(" "), Tui::bg(color.darkest.rgb, x)); - //let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); - //let height = pool.clips.read().unwrap().len() as u16; - w_exact(20, h_full(origin_n(iter( - ||pool.clips().clone().into_iter(), - move|clip: Arc>, i: usize|{ - let item_height = 1; - let item_offset = i as u16 * item_height; - let selected = i == pool.clip_index(); - let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); - let bg = if selected { color.light.rgb } else { color.base.rgb }; - let fg = color.lightest.rgb; - let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; - let length = if false { String::default() } else { format!("{length} ") }; - h_exact(1, iter_south(item_offset, item_height, Tui::bg(bg, below!( - w_full(origin_w(Tui::fg(fg, Tui::bold(selected, name)))), - w_full(origin_e(Tui::fg(fg, Tui::bold(selected, length)))), - w_full(origin_w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), - w_full(origin_e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), - )))) - })))) - } - } - impl ClipLength { - fn tui (&self) -> impl Draw { - use ClipLengthFocus::*; - let bars = ||self.bars_string(); - let beats = ||self.beats_string(); - let ticks = ||self.ticks_string(); - match self.focus { - None => east!(" ", bars(), ".", beats(), ".", ticks()), - Some(Bar) => east!("[", bars(), "]", beats(), ".", ticks()), - Some(Beat) => east!(" ", bars(), "[", beats(), "]", ticks()), - Some(Tick) => east!(" ", bars(), ".", beats(), "[", ticks()), - } - } - } -} - -mod config { - use crate::*; -} - -mod dialog { - use crate::*; - - impl Dialog { - /// ``` - /// let _ = tek::Dialog::welcome(); - /// ``` - pub fn welcome () -> Self { - Self::Menu(1, MenuItems([ - MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), - MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ - app.dialog = Dialog::None; - app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); - })))), - MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), - ].into())) - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().menu_selected(); - /// ``` - pub fn menu_selected (&self) -> Option { - if let Self::Menu(selected, _) = self { Some(*selected) } else { None } - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().menu_next(); - /// ``` - pub fn menu_next (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), - _ => Self::None - } - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().menu_prev(); - /// ``` - pub fn menu_prev (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), - _ => Self::None - } - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().device_kind(); - /// ``` - pub fn device_kind (&self) -> Option { - if let Self::Device(index) = self { Some(*index) } else { None } - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().device_kind_next(); - /// ``` - pub fn device_kind_next (&self) -> Option { - self.device_kind().map(|index|(index + 1) % device_kinds().len()) - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().device_kind_prev(); - /// ``` - pub fn device_kind_prev (&self) -> Option { - self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) - } - /// FIXME: implement - pub fn message (&self) -> Option<&str> { todo!() } - /// FIXME: implement - pub fn browser (&self) -> Option<&Arc> { todo!() } - /// FIXME: implement - pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } - } -} - -impl_audio!(|self: DeviceAudio<'a>, client, scope|{ - use Device::*; - match self.0 { - Mute => { Control::Continue }, - Bypass => { /*TODO*/ Control::Continue }, - #[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope), - #[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope), - #[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO - #[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO - #[cfg(feature = "clap")] Clap => { todo!() }, // TODO - #[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO - } -}); diff --git a/src/tick.rs b/src/tick.rs deleted file mode 100644 index c6bdd169..00000000 --- a/src/tick.rs +++ /dev/null @@ -1,168 +0,0 @@ -pub trait HasClock: AsRef + AsMut { - fn clock (&self) -> &Clock { self.as_ref() } - fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } -} - - -/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) -/// -/// ``` -/// let _ = tek::Timebase::default(); -/// ``` -#[derive(Debug, Clone)] pub struct Timebase { - /// Audio samples per second - pub sr: SampleRate, - /// MIDI beats per minute - pub bpm: Bpm, - /// MIDI ticks per beat - pub ppq: Ppq, -} - -/// Iterator that emits subsequent ticks within a range. -/// -/// ``` -/// let iter = tek::Ticker::default(); -/// ``` -#[derive(Debug, Default)] pub struct Ticker { - pub spp: f64, - pub sample: usize, - pub start: usize, - pub end: usize, -} - -/// A point in time in all time scales (microsecond, sample, MIDI pulse) -/// -/// ``` -/// let _ = tek::Moment::default(); -/// ``` -#[derive(Debug, Default, Clone)] pub struct Moment { - pub timebase: Arc, - /// Current time in microseconds - pub usec: Microsecond, - /// Current time in audio samples - pub sample: SampleCount, - /// Current time in MIDI pulses - pub pulse: Pulse, -} - -/// -/// ``` -/// let _ = tek::Moment2::default(); -/// ``` -#[derive(Debug, Clone, Default)] pub enum Moment2 { - #[default] None, - Zero, - Usec(Microsecond), - Sample(SampleCount), - Pulse(Pulse), -} - -/// MIDI resolution in PPQ (pulses per quarter note) -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64); - -/// Timestamp in MIDI pulses -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64); - -/// Tempo in beats per minute -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64); - -/// Quantization setting for launching clips -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64); - -/// Quantization setting for notes -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64); - -/// Timestamp in audio samples -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64); - -/// Audio sample rate in Hz (samples per second) -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64); - -/// Timestamp in microseconds -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64); - -/// The source of time. -/// -/// ``` -/// let clock = tek::Clock::default(); -/// ``` -#[derive(Clone, Default)] pub struct Clock { - /// JACK transport handle. - pub transport: Arc>, - /// Global temporal resolution (shared by [Moment] fields) - pub timebase: Arc, - /// Current global sample and usec (monotonic from JACK clock) - pub global: Arc, - /// Global sample and usec at which playback started - pub started: Arc>>, - /// Playback offset (when playing not from start) - pub offset: Arc, - /// Current playhead position - pub playhead: Arc, - /// Note quantization factor - pub quant: Arc, - /// Launch quantization factor - pub sync: Arc, - /// Size of buffer in samples - pub chunk: Arc, - // Cache of formatted strings - pub view_cache: Arc>, - /// For syncing the clock to an external source - #[cfg(feature = "port")] pub midi_in: Arc>>, - /// For syncing other devices to this clock - #[cfg(feature = "port")] pub midi_out: Arc>>, - /// For emitting a metronome - #[cfg(feature = "port")] pub click_out: Arc>>, -} - -/// A unit of time, represented as an atomic 64-bit float. -/// -/// According to https://stackoverflow.com/a/873367, as per IEEE754, -/// every integer between 1 and 2^53 can be represented exactly. -/// This should mean that, even at 192kHz sampling rate, over 1 year of audio -/// can be clocked in microseconds with f64 without losing precision. -pub trait TimeUnit: InteriorMutable {} - -/// Contains memoized renders of clock values. -/// -/// Performance optimization. -#[derive(Debug)] pub struct ClockView { - pub sr: Memo, String>, - pub buf: Memo, String>, - pub lat: Memo, String>, - pub bpm: Memo, String>, - pub beat: Memo, String>, - pub time: Memo, String>, -} From 244e2b388ef8bf9d8e6eeb8beb9b206ca3c5ea6d Mon Sep 17 00:00:00 2001 From: okay stopped screaming Date: Sat, 21 Mar 2026 23:29:20 +0200 Subject: [PATCH 5/7] wip: nomralize --- src/arrange.rs | 519 +++++++++-------- src/bind.rs | 130 +++++ src/browse.rs | 307 ++++++++--- src/cli.rs | 254 +++++++++ src/clock.rs | 530 +++++++++--------- src/config.rs | 92 +++ src/device.rs | 39 ++ src/dialog.rs | 106 ++++ src/menu.rs | 27 + src/mix.rs | 51 ++ src/mode.rs | 98 ++++ src/plugin.rs | 28 + src/sample.rs | 52 ++ src/sequence.rs | 92 +++ src/tek.rs | 1411 ++++------------------------------------------- src/view.rs | 10 + 16 files changed, 1880 insertions(+), 1866 deletions(-) create mode 100644 src/bind.rs create mode 100644 src/cli.rs create mode 100644 src/config.rs create mode 100644 src/dialog.rs create mode 100644 src/menu.rs create mode 100644 src/mode.rs create mode 100644 src/view.rs diff --git a/src/arrange.rs b/src/arrange.rs index 45417748..a2aaf6d4 100644 --- a/src/arrange.rs +++ b/src/arrange.rs @@ -1,7 +1,9 @@ +use crate::*; use ::std::sync::{Arc, RwLock}; use ::tengri::{space::east, color::ItemTheme}; use ::tengri::{draw::*, term::*}; use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput}; +impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } /// Arranger. /// @@ -48,6 +50,27 @@ use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput}; #[cfg(feature = "scene")] pub scene_scroll: usize, } +impl_has!(Jack<'static>: |self: Arrangement| self.jack); +impl_has!(Measure: |self: Arrangement| self.size); +impl_has!(Vec: |self: Arrangement| self.tracks); +impl_has!(Vec: |self: Arrangement| self.scenes); +impl_has!(Vec: |self: Arrangement| self.midi_ins); +impl_has!(Vec: |self: Arrangement| self.midi_outs); +impl_has!(Clock: |self: Arrangement| self.clock); +impl_has!(Selection: |self: Arrangement| self.selection); +impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); +impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); +impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); +impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); + +impl +AsMut> HasSelection for T {} +impl >+AsMut>> HasScenes for T {} +impl >+AsMut>> HasTracks for T {} +impl +AsMutOpt+Send+Sync> HasScene for T {} +impl +AsMutOpt+Send+Sync> HasTrack for T {} +impl > TracksView for T {} +impl ClipsView for T {} + pub trait ClipsView: TracksView + ScenesView { fn view_scenes_clips <'a> (&'a self) @@ -674,247 +697,277 @@ impl Selection { } } } - impl_has!(Jack<'static>: |self: Arrangement| self.jack); - impl_has!(Measure: |self: Arrangement| self.size); - impl_has!(Vec: |self: Arrangement| self.tracks); - impl_has!(Vec: |self: Arrangement| self.scenes); - impl_has!(Vec: |self: Arrangement| self.midi_ins); - impl_has!(Vec: |self: Arrangement| self.midi_outs); - impl_has!(Clock: |self: Arrangement| self.clock); - impl_has!(Selection: |self: Arrangement| self.selection); - impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); - impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); - impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); - impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); - impl Arrangement { - /// Create a new arrangement. - pub fn new ( - jack: &Jack<'static>, - name: Option>, - clock: Clock, - tracks: Vec, - scenes: Vec, - midi_ins: Vec, - midi_outs: Vec, - ) -> Self { - Self { - clock, tracks, scenes, midi_ins, midi_outs, - jack: jack.clone(), - name: name.unwrap_or_default(), - color: ItemTheme::random(), - selection: Selection::TrackClip { track: 0, scene: 0 }, - ..Default::default() +impl Arrangement { + /// Create a new arrangement. + pub fn new ( + jack: &Jack<'static>, + name: Option>, + clock: Clock, + tracks: Vec, + scenes: Vec, + midi_ins: Vec, + midi_outs: Vec, + ) -> Self { + Self { + clock, tracks, scenes, midi_ins, midi_outs, + jack: jack.clone(), + name: name.unwrap_or_default(), + color: ItemTheme::random(), + selection: Selection::TrackClip { track: 0, scene: 0 }, + ..Default::default() + } + } + /// Width of display + pub fn w (&self) -> u16 { + self.size.w() as u16 + } + /// Width allocated for sidebar. + pub fn w_sidebar (&self, is_editing: bool) -> u16 { + self.w() / if is_editing { 16 } else { 8 } as u16 + } + /// Width available to display tracks. + pub fn w_tracks_area (&self, is_editing: bool) -> u16 { + self.w().saturating_sub(self.w_sidebar(is_editing)) + } + /// Height of display + pub fn h (&self) -> u16 { + self.size.h() as u16 + } + /// Height taken by visible device slots. + pub fn h_devices (&self) -> u16 { + 2 + //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) + } + /// Add multiple tracks + #[cfg(feature = "track")] pub fn tracks_add ( + &mut self, + count: usize, width: Option, + mins: &[Connect], mouts: &[Connect], + ) -> Usually<()> { + 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 track = self.track_add(None, Some(color), mins, mouts)?.1; + if let Some(width) = width { + track.width = width; } } - /// Width of display - pub fn w (&self) -> u16 { - self.size.w() as u16 + Ok(()) + } + /// Add a track + #[cfg(feature = "track")] pub fn track_add ( + &mut self, + name: Option<&str>, color: Option, + mins: &[Connect], mouts: &[Connect], + ) -> Usually<(usize, &mut Track)> { + let name: Arc = name.map_or_else( + ||format!("trk{:02}", self.track_last).into(), + |x|x.to_string().into() + ); + self.track_last += 1; + let track = Track { + width: (name.len() + 2).max(12), + color: color.unwrap_or_else(ItemTheme::random), + sequencer: Sequencer::new( + &format!("{name}"), + self.jack(), + Some(self.clock()), + None, + mins, + mouts + )?, + name, + ..Default::default() + }; + self.tracks_mut().push(track); + let len = self.tracks().len(); + let index = len - 1; + for scene in self.scenes_mut().iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); + } } - /// Width allocated for sidebar. - pub fn w_sidebar (&self, is_editing: bool) -> u16 { - self.w() / if is_editing { 16 } else { 8 } as u16 - } - /// Width available to display tracks. - pub fn w_tracks_area (&self, is_editing: bool) -> u16 { - self.w().saturating_sub(self.w_sidebar(is_editing)) - } - /// Height of display - pub fn h (&self) -> u16 { - self.size.h() as u16 - } - /// Height taken by visible device slots. - pub fn h_devices (&self) -> u16 { - 2 - //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) - } - /// Add multiple tracks - #[cfg(feature = "track")] pub fn tracks_add ( - &mut self, - count: usize, width: Option, - mins: &[Connect], mouts: &[Connect], - ) -> Usually<()> { - 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 track = self.track_add(None, Some(color), mins, mouts)?.1; - if let Some(width) = width { - track.width = width; + Ok((index, &mut self.tracks_mut()[index])) + } + #[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { + south( + h_exact(1, self.view_inputs_header()), + Thunk::new(|to: &mut Tui|{ + for (index, port) in self.midi_ins().iter().enumerate() { + to.place(&x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port)))) } - } - Ok(()) - } - /// Add a track - #[cfg(feature = "track")] pub fn track_add ( - &mut self, - name: Option<&str>, color: Option, - mins: &[Connect], mouts: &[Connect], - ) -> Usually<(usize, &mut Track)> { - let name: Arc = name.map_or_else( - ||format!("trk{:02}", self.track_last).into(), - |x|x.to_string().into() - ); - self.track_last += 1; - let track = Track { - width: (name.len() + 2).max(12), - color: color.unwrap_or_else(ItemTheme::random), - sequencer: Sequencer::new( - &format!("{name}"), - self.jack(), - Some(self.clock()), - None, - mins, - mouts - )?, - name, - ..Default::default() - }; - self.tracks_mut().push(track); - let len = self.tracks().len(); - let index = len - 1; - for scene in self.scenes_mut().iter_mut() { - while scene.clips.len() < len { - scene.clips.push(None); - } - } - Ok((index, &mut self.tracks_mut()[index])) - } - #[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { - south( - h_exact(1, self.view_inputs_header()), - Thunk::new(|to: &mut Tui|{ - for (index, port) in self.midi_ins().iter().enumerate() { - to.place(&x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port)))) - } - }) - ) - } - #[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Draw + '_ { - east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), - west(w_exact(4, button_2("I", "+", false)), Thunk::new(move|to: &mut Tui|for (_index, track, x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - to.place(&x_push(x1 as u16, Tui::bg(track.color.dark.rgb, origin_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), - either(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), - either(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), - )))))) - }))) - } - #[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Draw { - east(w_exact(20, origin_w(east(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), - west(w_exact(4, ()), Thunk::new(move|to: &mut Tui|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - to.place(&Tui::bg(track.color.darker.rgb, origin_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), - either(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), - either(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), - ))))) - }))) - } - #[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { - let mut h = 1; - for output in self.midi_outs().iter() { - h += 1 + output.connections.len(); - } - let h = h as u16; - let list = south( - h_exact(1, w_full(origin_w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), - h_exact(h - 1, wh_full(origin_nw(Thunk::new(|to: &mut Tui|{ - for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&h_exact(1,w_full(east( - origin_w(east(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), - w_full(origin_e(format!("{}/{} ", - port.port().get_connections().len(), - port.connections.len()))))))); - for (index, conn) in port.connections.iter().enumerate() { - to.place(&h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info()))))); - } - } - }))))); - h_exact(h, view_track_row_section(theme, list, button_2("O", "+", false), - Tui::bg(theme.darker.rgb, origin_w(w_full( - Thunk::new(|to: &mut Tui|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&w_exact(track_width(index, track), - Thunk::new(|to: &mut Tui|{ - to.place(&h_exact(1, origin_w(east( - either(true, Tui::fg(Green, "play "), "play "), - either(false, Tui::fg(Yellow, "solo "), "solo "), - )))); - for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&h_exact(1, origin_w(east( - either(true, Tui::fg(Green, " ● "), " · "), - either(false, Tui::fg(Yellow, " ● "), " · "), - )))); - for (_index, _conn) in port.connections.iter().enumerate() { - to.place(&h_exact(1, w_full(""))); - } - }})))}})))))) - } - #[cfg(feature = "track")] pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { - let mut h = 2u16; - for track in self.tracks().iter() { - h = h.max(track.devices.len() as u16 * 2); - } - view_track_row_section(theme, - button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), - button_2("D", "+", false), - Thunk::new(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&wh_exact(track_width(index, track), h + 1, - Tui::bg(track.color.dark.rgb, origin_nw(iter_south(2, move||0..h, - |_, _index|wh_exact(track.width as u16, 2, - Tui::fg_bg( - ItemTheme::G[32].lightest.rgb, - ItemTheme::G[32].dark.rgb, - origin_nw(format!(" · {}", "--"))))))))); - })) - } - /// Put a clip in a slot - #[cfg(feature = "clip")] pub fn clip_put ( - &mut self, track: usize, scene: usize, clip: Option>> - ) -> Option>> { - let old = self.scenes[scene].clips[track].clone(); - self.scenes[scene].clips[track] = clip; - old - } - /// Change the color of a clip, returning the previous one - #[cfg(feature = "clip")] pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) - -> Option - { - self.scenes[scene].clips[track].as_ref().map(|clip|{ - let mut clip = clip.write().unwrap(); - let old = clip.color.clone(); - clip.color = color.clone(); - panic!("{color:?} {old:?}"); - //old }) + ) + } + #[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Draw + '_ { + east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), + west(w_exact(4, button_2("I", "+", false)), Thunk::new(move|to: &mut Tui|for (_index, track, x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] + to.place(&x_push(x1 as u16, Tui::bg(track.color.dark.rgb, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), + either(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), + either(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), + )))))) + }))) + } + #[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Draw { + east(w_exact(20, origin_w(east(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), + west(w_exact(4, ()), Thunk::new(move|to: &mut Tui|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] + to.place(&Tui::bg(track.color.darker.rgb, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), + either(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), + either(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), + ))))) + }))) + } + #[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { + let mut h = 1; + for output in self.midi_outs().iter() { + h += 1 + output.connections.len(); } - /// Toggle looping for the active clip - #[cfg(feature = "clip")] pub fn toggle_loop (&mut self) { - if let Some(clip) = self.selected_clip() { - clip.write().unwrap().toggle_loop() - } + let h = h as u16; + let list = south( + h_exact(1, w_full(origin_w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), + h_exact(h - 1, wh_full(origin_nw(Thunk::new(|to: &mut Tui|{ + for (_index, port) in self.midi_outs().iter().enumerate() { + to.place(&h_exact(1,w_full(east( + origin_w(east(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), + w_full(origin_e(format!("{}/{} ", + port.port().get_connections().len(), + port.connections.len()))))))); + for (index, conn) in port.connections.iter().enumerate() { + to.place(&h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info()))))); + } + } + }))))); + h_exact(h, view_track_row_section(theme, list, button_2("O", "+", false), + Tui::bg(theme.darker.rgb, origin_w(w_full( + Thunk::new(|to: &mut Tui|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&w_exact(track_width(index, track), + Thunk::new(|to: &mut Tui|{ + to.place(&h_exact(1, origin_w(east( + either(true, Tui::fg(Green, "play "), "play "), + either(false, Tui::fg(Yellow, "solo "), "solo "), + )))); + for (_index, port) in self.midi_outs().iter().enumerate() { + to.place(&h_exact(1, origin_w(east( + either(true, Tui::fg(Green, " ● "), " · "), + either(false, Tui::fg(Yellow, " ● "), " · "), + )))); + for (_index, _conn) in port.connections.iter().enumerate() { + to.place(&h_exact(1, w_full(""))); + } + }})))}})))))) + } + #[cfg(feature = "track")] pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { + let mut h = 2u16; + for track in self.tracks().iter() { + h = h.max(track.devices.len() as u16 * 2); } - /// Get the first sampler of the active track - #[cfg(feature = "sampler")] pub fn sampler (&self) -> Option<&Sampler> { - self.selected_track()?.sampler(0) - } - /// Get the first sampler of the active track - #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.selected_track_mut()?.sampler_mut(0) + view_track_row_section(theme, + button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), + button_2("D", "+", false), + Thunk::new(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&wh_exact(track_width(index, track), h + 1, + Tui::bg(track.color.dark.rgb, origin_nw(iter_south(2, move||0..h, + |_, _index|wh_exact(track.width as u16, 2, + Tui::fg_bg( + ItemTheme::G[32].lightest.rgb, + ItemTheme::G[32].dark.rgb, + origin_nw(format!(" · {}", "--"))))))))); + })) + } + /// Put a clip in a slot + #[cfg(feature = "clip")] pub fn clip_put ( + &mut self, track: usize, scene: usize, clip: Option>> + ) -> Option>> { + let old = self.scenes[scene].clips[track].clone(); + self.scenes[scene].clips[track] = clip; + old + } + /// Change the color of a clip, returning the previous one + #[cfg(feature = "clip")] pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) + -> Option + { + self.scenes[scene].clips[track].as_ref().map(|clip|{ + let mut clip = clip.write().unwrap(); + let old = clip.color.clone(); + clip.color = color.clone(); + panic!("{color:?} {old:?}"); + //old + }) + } + /// Toggle looping for the active clip + #[cfg(feature = "clip")] pub fn toggle_loop (&mut self) { + if let Some(clip) = self.selected_clip() { + clip.write().unwrap().toggle_loop() } } - impl ScenesView for Arrangement { - fn h_scenes (&self) -> u16 { - (self.measure_height() as u16).saturating_sub(20) - } - fn w_side (&self) -> u16 { - (self.measure_width() as u16 * 2 / 10).max(20) - } - fn w_mid (&self) -> u16 { - (self.measure_width() as u16).saturating_sub(2 * self.w_side()).max(40) - } + /// Get the first sampler of the active track + #[cfg(feature = "sampler")] pub fn sampler (&self) -> Option<&Sampler> { + self.selected_track()?.sampler(0) } - impl HasClipsSize for Arrangement { - fn clips_size (&self) -> &Measure { &self.size_inner } + /// Get the first sampler of the active track + #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { + self.selected_track_mut()?.sampler_mut(0) } +} +impl ScenesView for Arrangement { + fn h_scenes (&self) -> u16 { + (self.measure_height() as u16).saturating_sub(20) + } + fn w_side (&self) -> u16 { + (self.measure_width() as u16 * 2 / 10).max(20) + } + fn w_mid (&self) -> u16 { + (self.measure_width() as u16).saturating_sub(2 * self.w_side()).max(40) + } +} +impl HasClipsSize for Arrangement { + fn clips_size (&self) -> &Measure { &self.size_inner } +} + +pub type SceneWith<'a, T> = + (usize, &'a Scene, usize, usize, T); + +def_command!(SceneCommand: |scene: Scene| { + SetSize { size: usize } => { todo!() }, + SetZoom { size: usize } => { todo!() }, + SetName { name: Arc } => + swap_value(&mut scene.name, name, |name|Self::SetName{name}), + SetColor { color: ItemTheme } => + swap_value(&mut scene.color, color, |color|Self::SetColor{color}), +}); + +def_command!(TrackCommand: |track: Track| { + Stop => { track.sequencer.enqueue_next(None); Ok(None) }, + SetMute { mute: Option } => todo!(), + SetSolo { solo: Option } => todo!(), + SetSize { size: usize } => todo!(), + SetZoom { zoom: usize } => todo!(), + SetName { name: Arc } => + swap_value(&mut track.name, name, |name|Self::SetName { name }), + SetColor { color: ItemTheme } => + swap_value(&mut track.color, color, |color|Self::SetColor { color }), + SetRec { rec: Option } => + toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }), + SetMon { mon: Option } => + toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }), +}); + +def_command!(ClipCommand: |clip: MidiClip| { + SetColor { color: Option } => { + //(SetColor [t: usize, s: usize, c: ItemTheme] + //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); + //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) + todo!() + }, + SetLoop { looping: Option } => { + //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) + //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) + todo!() + } +}); diff --git a/src/bind.rs b/src/bind.rs new file mode 100644 index 00000000..a8eaae7d --- /dev/null +++ b/src/bind.rs @@ -0,0 +1,130 @@ +use crate::*; +/// A control axis. +/// +/// ``` +/// let axis = tek::ControlAxis::X; +/// ``` +#[derive(Debug, Copy, Clone)] pub enum ControlAxis { + X, Y, Z, I +} + +/// Collection of input bindings. +pub type Binds = Arc, Bind>>>>; + +pub(crate) fn load_bind (binds: &Binds, name: &impl AsRef, body: &impl Language) -> Usually<()> { + binds.write().unwrap().insert(name.as_ref().into(), Bind::load(body)?); + Ok(()) +} + +/// An map of input events (e.g. [TuiEvent]) to [Binding]s. +/// +/// ``` +/// let lang = "(@x (nop)) (@y (nop) (nop))"; +/// let bind = tek::Bind::>::load(&lang).unwrap(); +/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1)); +/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2)); +/// ``` +#[derive(Debug)] pub struct Bind( + /// Map of each event (e.g. key combination) to + /// all command expressions bound to it by + /// all loaded input layers. + pub BTreeMap>> +); + +/// A sequence of zero or more commands (e.g. [AppCommand]), +/// optionally filtered by [Condition] to form layers. +/// +/// ``` +/// //FIXME: Why does it overflow? +/// //let binding: Binding<()> = tek::Binding { ..Default::default() }; +/// ``` +#[derive(Debug, Clone)] pub struct Binding { + pub commands: Arc<[C]>, + pub condition: Option, + pub description: Option>, + pub source: Option>, +} + +/// Condition that must evaluate to true in order to enable an input layer. +/// +/// ``` +/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true}))); +/// ``` +#[derive(Clone)] pub struct Condition( + pub Arcbool + Send + Sync>> +); + +impl Bind> { + pub fn load (lang: &impl Language) -> Usually { + let mut map = Bind::new(); + lang.each(|item|if item.expr().head() == Ok(Some("see")) { + // TODO + Ok(()) + } else if let Ok(Some(_word)) = item.expr().head().word() { + if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { + map.add(key, Binding { + commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), + condition: None, + description: None, + source: None + }); + Ok(()) + } else if Some(":char") == item.expr()?.head()? { + // TODO + return Ok(()) + } else { + return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) + } + } else { + return Err(format!("Config::load_bind: unexpected: {item:?}").into()) + })?; + Ok(map) + } +} + +/// Default is always empty map regardless if `E` and `C` implement [Default]. +impl Default for Bind { + fn default () -> Self { Self(Default::default()) } +} +impl Default for Binding { + fn default () -> Self { + Self { + commands: Default::default(), + condition: Default::default(), + description: Default::default(), + source: Default::default(), + } + } +} + +impl Bind { + /// Create a new event map + pub fn new () -> Self { + Default::default() + } + /// Add a binding to an owned event map. + pub fn def (mut self, event: E, binding: Binding) -> Self { + self.add(event, binding); + self + } + /// Add a binding to an event map. + pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { + if !self.0.contains_key(&event) { + self.0.insert(event.clone(), Default::default()); + } + self.0.get_mut(&event).unwrap().push(binding); + self + } + /// Return the binding(s) that correspond to an event. + pub fn query (&self, event: &E) -> Option<&[Binding]> { + self.0.get(event).map(|x|x.as_slice()) + } + /// Return the first binding that corresponds to an event, considering conditions. + pub fn dispatch (&self, event: &E) -> Option<&Binding> { + self.query(event) + .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) + .flatten() + } +} + +impl_debug!(Condition |self, w| { write!(w, "*") }); diff --git a/src/browse.rs b/src/browse.rs index f9ca478b..48dda692 100644 --- a/src/browse.rs +++ b/src/browse.rs @@ -1,7 +1,17 @@ +use crate::*; use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; use crate::sequence::MidiClip; use crate::sample::Sample; +def_command!(FileBrowserCommand: |sampler: Sampler|{ + //("begin" [] Some(Self::Begin)) + //("cancel" [] Some(Self::Cancel)) + //("confirm" [] Some(Self::Confirm)) + //("select" [i: usize] Some(Self::Select(i.expect("no index")))) + //("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path")))) + //("filter" [f: Arc] Some(Self::Filter(f.expect("no filter"))))) +}); + /// Browses for files to load/save. /// /// ``` @@ -276,76 +286,247 @@ impl ClipLength { } } } - impl Browse { - pub fn new (cwd: Option) -> Usually { - let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; - let mut dirs = vec![]; - let mut files = vec![]; - for entry in std::fs::read_dir(&cwd)? { - let entry = entry?; - let name = entry.file_name(); - let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); - let meta = entry.metadata()?; - if meta.is_dir() { - dirs.push((name, format!("📁 {decoded}"))); - } else if meta.is_file() { - files.push((name, format!("📄 {decoded}"))); + +impl Browse { + pub fn new (cwd: Option) -> Usually { + let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; + let mut dirs = vec![]; + let mut files = vec![]; + for entry in std::fs::read_dir(&cwd)? { + let entry = entry?; + let name = entry.file_name(); + let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); + let meta = entry.metadata()?; + if meta.is_dir() { + dirs.push((name, format!("📁 {decoded}"))); + } else if meta.is_file() { + files.push((name, format!("📄 {decoded}"))); + } + } + Ok(Self { cwd, dirs, files, ..Default::default() }) + } + pub fn chdir (&self) -> Usually { Self::new(Some(self.path())) } + pub fn len (&self) -> usize { self.dirs.len() + self.files.len() } + pub fn is_dir (&self) -> bool { self.index < self.dirs.len() } + pub fn is_file (&self) -> bool { self.index >= self.dirs.len() } + pub fn path (&self) -> PathBuf { + self.cwd.join(if self.is_dir() { + &self.dirs[self.index].0 + } else if self.is_file() { + &self.files[self.index - self.dirs.len()].0 + } else { + unreachable!() + }) + } + fn _todo_stub_path_buf (&self) -> PathBuf { todo!() } + fn _todo_stub_usize (&self) -> usize { todo!() } + fn _todo_stub_arc_str (&self) -> Arc { todo!() } +} +impl Browse { + fn tui (&self) -> impl Draw { + iter_south(1, ||EntriesIterator { + offset: 0, + index: 0, + length: self.dirs.len() + self.files.len(), + browser: self, + }, |entry, _index|w_full(origin_w(entry))) + } +} +impl<'a> Iterator for EntriesIterator<'a> { + type Item = Modify<&'a str>; + fn next (&mut self) -> Option { + let dirs = self.browser.dirs.len(); + let files = self.browser.files.len(); + let index = self.index; + if self.index < dirs { + self.index += 1; + Some(Tui::bold(true, self.browser.dirs[index].1.as_str())) + } else if self.index < dirs + files { + self.index += 1; + Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str())) + } else { + None + } + } +} +impl PartialEq for BrowseTarget { + fn eq (&self, other: &Self) -> bool { + match self { + Self::ImportSample(_) => false, + Self::ExportSample(_) => false, + Self::ImportClip(_) => false, + Self::ExportClip(_) => false, + #[allow(unused)] t => matches!(other, t) + } + } +} + +def_command!(BrowseCommand: |browse: Browse| { + SetVisible => Ok(None), + SetPath { address: PathBuf } => Ok(None), + SetSearch { filter: Arc } => Ok(None), + SetCursor { cursor: usize } => Ok(None), +}); + +def_command!(PoolCommand: |pool: Pool| { + // Toggle visibility of pool + Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) }, + // Select a clip from the clip pool + Select { index: usize } => { pool.set_clip_index(*index); Ok(None) }, + // Update the contents of the clip pool + Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})), + // Rename a clip + Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?), + // Change the length of a clip + Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?), + // Import from file + Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { + command.delegate(browse, |command|Self::Import{command})? + } else { + None + }), + // Export to file + Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { + command.delegate(browse, |command|Self::Export{command})? + } else { + None + }), +}); + +def_command!(PoolClipCommand: |pool: Pool| { + Delete { index: usize } => { + let index = *index; + let clip = pool.clips_mut().remove(index).read().unwrap().clone(); + Ok(Some(Self::Add { index, clip })) + }, + Swap { index: usize, other: usize } => { + let index = *index; + let other = *other; + pool.clips_mut().swap(index, other); + Ok(Some(Self::Swap { index, other })) + }, + Export { index: usize, path: PathBuf } => { + todo!("export clip to midi file"); + }, + Add { index: usize, clip: MidiClip } => { + let index = *index; + let mut index = index; + let clip = Arc::new(RwLock::new(clip.clone())); + let mut clips = pool.clips_mut(); + if index >= clips.len() { + index = clips.len(); + clips.push(clip) + } else { + clips.insert(index, clip); + } + Ok(Some(Self::Delete { index })) + }, + Import { index: usize, path: PathBuf } => { + let index = *index; + let bytes = std::fs::read(&path)?; + let smf = Smf::parse(bytes.as_slice())?; + let mut t = 0u32; + let mut events = vec![]; + for track in smf.tracks.iter() { + for event in track.iter() { + t += event.delta.as_int(); + if let TrackEventKind::Midi { channel, message } = event.kind { + events.push((t, channel.as_int(), message)); } } - Ok(Self { cwd, dirs, files, ..Default::default() }) } - pub fn chdir (&self) -> Usually { Self::new(Some(self.path())) } - pub fn len (&self) -> usize { self.dirs.len() + self.files.len() } - pub fn is_dir (&self) -> bool { self.index < self.dirs.len() } - pub fn is_file (&self) -> bool { self.index >= self.dirs.len() } - pub fn path (&self) -> PathBuf { - self.cwd.join(if self.is_dir() { - &self.dirs[self.index].0 - } else if self.is_file() { - &self.files[self.index - self.dirs.len()].0 - } else { - unreachable!() - }) + let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None); + for event in events.iter() { + clip.notes[event.0 as usize].push(event.2); } - fn _todo_stub_path_buf (&self) -> PathBuf { todo!() } - fn _todo_stub_usize (&self) -> usize { todo!() } - fn _todo_stub_arc_str (&self) -> Arc { todo!() } - } - impl Browse { - fn tui (&self) -> impl Draw { - iter_south(1, ||EntriesIterator { - offset: 0, - index: 0, - length: self.dirs.len() + self.files.len(), - browser: self, - }, |entry, _index|w_full(origin_w(entry))) + Ok(Self::Add { index, clip }.execute(pool)?) + }, + SetName { index: usize, name: Arc } => { + let index = *index; + let clip = &mut pool.clips_mut()[index]; + let old_name = clip.read().unwrap().name.clone(); + clip.write().unwrap().name = name.clone(); + Ok(Some(Self::SetName { index, name: old_name })) + }, + SetLength { index: usize, length: usize } => { + let index = *index; + let clip = &mut pool.clips_mut()[index]; + let old_len = clip.read().unwrap().length; + clip.write().unwrap().length = *length; + Ok(Some(Self::SetLength { index, length: old_len })) + }, + SetColor { index: usize, color: ItemColor } => { + let index = *index; + let mut color = ItemTheme::from(*color); + std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color); + Ok(Some(Self::SetColor { index, color: color.base })) + }, +}); + +def_command!(RenameCommand: |pool: Pool| { + Begin => unreachable!(), + Cancel => { + if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { + pool.clips()[clip].write().unwrap().name = old_name.clone().into(); } - } - impl<'a> Iterator for EntriesIterator<'a> { - type Item = Modify<&'a str>; - fn next (&mut self) -> Option { - let dirs = self.browser.dirs.len(); - let files = self.browser.files.len(); - let index = self.index; - if self.index < dirs { - self.index += 1; - Some(Tui::bold(true, self.browser.dirs[index].1.as_str())) - } else if self.index < dirs + files { - self.index += 1; - Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str())) - } else { - None + Ok(None) + }, + Confirm => { + if let Some(PoolMode::Rename(_clip, ref mut old_name)) = pool.mode_mut().clone() { + let old_name = old_name.clone(); *pool.mode_mut() = None; return Ok(Some(Self::Set { value: old_name })) + } + Ok(None) + }, + Set { value: Arc } => { + if let Some(PoolMode::Rename(clip, ref mut _old_name)) = pool.mode_mut().clone() { + pool.clips()[clip].write().unwrap().name = value.clone(); + } + Ok(None) + }, +}); + +def_command!(CropCommand: |pool: Pool| { + Begin => unreachable!(), + Cancel => { if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { *pool.mode_mut() = None; } Ok(None) }, + Set { length: usize } => { + if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus)) + = pool.mode_mut().clone() + { + let old_length; + { + let clip = pool.clips()[clip].clone();//.write().unwrap(); + old_length = Some(clip.read().unwrap().length); + clip.write().unwrap().length = *length; + } + *pool.mode_mut() = None; + return Ok(old_length.map(|length|Self::Set { length })) + } + Ok(None) + }, + Next => { + if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.next() }; Ok(None) + }, + Prev => { + if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.prev() }; Ok(None) + }, + Inc => { + if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() { + match focus { + ClipLengthFocus::Bar => { *length += 4 * PPQ }, + ClipLengthFocus::Beat => { *length += PPQ }, + ClipLengthFocus::Tick => { *length += 1 }, } } - } - impl PartialEq for BrowseTarget { - fn eq (&self, other: &Self) -> bool { - match self { - Self::ImportSample(_) => false, - Self::ExportSample(_) => false, - Self::ImportClip(_) => false, - Self::ExportClip(_) => false, - #[allow(unused)] t => matches!(other, t) + Ok(None) + }, + Dec => { + if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() { + match focus { + ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) }, + ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) }, + ClipLengthFocus::Tick => { *length = length.saturating_sub(1) }, } } + Ok(None) } +}); diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 00000000..6c8bc6cf --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,254 @@ +use crate::*; +use crate::*; + +/// The command-line interface descriptor. +/// +/// ``` +/// let cli: tek::Cli = Default::default(); +/// +/// use clap::CommandFactory; +/// tek::Cli::command().debug_assert(); +/// ``` +#[derive(Parser)] +#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] +#[derive(Debug, Default)] pub struct Cli { + /// Pre-defined configuration modes. + /// + /// TODO: Replace these with scripted configurations. + #[command(subcommand)] pub action: Action, +} + +/// Application modes that can be passed to the mommand line interface. +/// +/// ``` +/// let action: tek::Action = Default::default(); +/// ``` +#[derive(Debug, Clone, Subcommand, Default)] pub enum Action { + /// Continue where you left off + #[default] Resume, + /// Run headlessly in current session. + Headless, + /// Show status of current session. + Status, + /// List known sessions. + List, + /// Continue work in a copy of the current session. + Fork, + /// Create a new empty session. + New { + /// Name of JACK client + #[arg(short='n', long)] name: Option, + /// Whether to attempt to become transport master + #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, + /// Whether to sync to external transport master + #[arg(short='y', long, default_value_t = true)] sync_follow: bool, + /// Initial tempo in beats per minute + #[arg(short='b', long, default_value = None)] bpm: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short='c', long, default_value_t = true)] show_clock: bool, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='I', long)] midi_from: Vec, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='i', long)] midi_from_re: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='O', long)] midi_to: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='o', long)] midi_to_re: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] left_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] right_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] left_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] right_to: Vec, + /// Tracks to create + #[arg(short='t', long)] tracks: Option, + /// Scenes to create + #[arg(short='s', long)] scenes: Option, + }, + /// Import media as new session. + Import, + /// Show configuration. + Config, + /// Show version. + Version, +} + +/// Command-line configuration. +#[cfg(feature = "cli")] impl Cli { + pub fn run (&self) -> Usually<()> { + if let Action::Version = self.action { + return Ok(tek_show_version()) + } + + let mut config = Config::new(None); + config.init()?; + + if let Action::Config = self.action { + tek_print_config(&config); + } else if let Action::List = self.action { + todo!("list sessions") + } else if let Action::Resume = self.action { + todo!("resume session") + } else if let Action::New { + name, bpm, tracks, scenes, sync_lead, sync_follow, + midi_from, midi_from_re, midi_to, midi_to_re, + left_from, right_from, left_to, right_to, .. + } = &self.action { + + // Connect to JACK + let name = name.as_ref().map_or("tek", |x|x.as_str()); + let jack = Jack::new(&name)?; + + // TODO: Collect audio IO: + let empty = &[] as &[&str]; + let left_froms = Connect::collect(&left_from, empty, empty); + let left_tos = Connect::collect(&left_to, empty, empty); + let right_froms = Connect::collect(&right_from, empty, empty); + let right_tos = Connect::collect(&right_to, empty, empty); + let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; + let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; + + // Create initial project: + let clock = Clock::new(&jack, *bpm)?; + let mut project = Arrangement::new( + &jack, + None, + clock, + vec![], + vec![], + Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() + .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) + .collect::, _>>()?, + Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() + .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) + .collect::, _>>()? + ); + project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; + project.scenes_add(scenes.unwrap_or(0))?; + + if matches!(self.action, Action::Status) { + // Show status and exit + tek_print_status(&project); + return Ok(()) + } + + // Initialize the app state + let app = tek(&jack, project, config, ":menu"); + if matches!(self.action, Action::Headless) { + // TODO: Headless mode (daemon + client over IPC, then over network...) + println!("todo headless"); + return Ok(()) + } + + // Run the [Tui] and [Jack] threads with the [App] state. + Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ + + // Between jack init and app's first cycle: + + jack.sync_lead(*sync_lead, |mut state|{ + let clock = app.clock(); + clock.playhead.update_from_sample(state.position.frame() as f64); + state.position.bbt = Some(clock.bbt()); + state.position + })?; + + jack.sync_follow(*sync_follow)?; + + // FIXME: They don't work properly. + + Ok(app) + + })?)?; + } + Ok(()) + } +} + +pub fn tek_show_version () { + println!("todo version"); +} + +pub fn tek_print_config (config: &Config) { + use ::ansi_term::Color::*; + println!("{:?}", config.dirs); + for (k, v) in config.views.read().unwrap().iter() { + println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}"))); + } + for (k, v) in config.binds.read().unwrap().iter() { + println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}"))); + for (k, v) in v.0.iter() { + print!("{} ", &Yellow.paint(match &k.0 { + Event::Key(KeyEvent { modifiers, .. }) => + format!("{:>16}", format!("{modifiers}")), + _ => unimplemented!() + })); + print!("{}", &Yellow.bold().paint(match &k.0 { + Event::Key(KeyEvent { code, .. }) => + format!("{:<10}", format!("{code}")), + _ => unimplemented!() + })); + for v in v.iter() { + print!(" => {:?}", v.commands); + print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default()); + println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default()); + //println!(" {:?}", v.source); + } + } + } + for (k, v) in config.modes.read().unwrap().iter() { + println!(); + for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); } + for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); } + print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}"))); + print!("\n{}", Blue.paint("KEYS")); + for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); } + println!(); + for (k, v) in v.modes.read().unwrap().iter() { + print!("{} {} {:?}", + Blue.paint("MODE"), + Green.bold().paint(format!("{k:<16}")), + v.name); + print!(" INFO={:?}", + v.info); + print!(" VIEW={:?}", + v.view); + println!(" KEYS={:?}", + v.keys); + } + print!("{}", Blue.paint("VIEW")); + for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); } + println!(); + } +} + +pub fn tek_print_status (project: &Arrangement) { + println!("Name: {:?}", &project.name); + println!("JACK: {:?}", &project.jack); + println!("Buffer: {:?}", &project.clock.chunk); + println!("Sample rate: {:?}", &project.clock.timebase.sr); + println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); + println!("Tempo: {:?}", &project.clock.timebase.bpm); + println!("Quantize: {:?}", &project.clock.quant); + println!("Launch: {:?}", &project.clock.sync); + println!("Playhead: {:?}us", &project.clock.playhead.usec); + println!("Playhead: {:?}s", &project.clock.playhead.sample); + println!("Playhead: {:?}p", &project.clock.playhead.pulse); + println!("Started: {:?}", &project.clock.started); + println!("Tracks:"); + for (i, t) in project.tracks.iter().enumerate() { + println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, + &t.sequencer.play_clip, &t.sequencer.next_clip); + } + println!("Scenes:"); + for (i, t) in project.scenes.iter().enumerate() { + println!(" Scene {i}: {} {:?}", &t.name, &t.clips); + } + println!("MIDI Ins: {:?}", &project.midi_ins); + println!("MIDI Outs: {:?}", &project.midi_outs); + println!("Audio Ins: {:?}", &project.audio_ins); + println!("Audio Outs: {:?}", &project.audio_outs); + // TODO git integration + // TODO dawvert integration +} diff --git a/src/clock.rs b/src/clock.rs index 2d78af28..16c8de23 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -1,3 +1,4 @@ +use crate::*; use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; use ::atomic_float::AtomicF64; use ::tengri::{draw::*, term::*}; @@ -425,33 +426,276 @@ impl Microsecond { /// Define and implement a unit of time #[macro_export] macro_rules! impl_time_unit { -($T:ident) => { - impl Gettable for $T { - fn get (&self) -> f64 { self.0.load(Relaxed) } - } - impl InteriorMutable for $T { - fn set (&self, value: f64) -> f64 { - let old = self.get(); - self.0.store(value, Relaxed); - old + ($T:ident) => { + impl Gettable for $T { + fn get (&self) -> f64 { self.0.load(Relaxed) } } + impl InteriorMutable for $T { + fn set (&self, value: f64) -> f64 { + let old = self.get(); + self.0.store(value, Relaxed); + old + } + } + impl TimeUnit for $T {} + impl_op!($T, Add, add, |a, b|{a + b}); + impl_op!($T, Sub, sub, |a, b|{a - b}); + impl_op!($T, Mul, mul, |a, b|{a * b}); + impl_op!($T, Div, div, |a, b|{a / b}); + impl_op!($T, Rem, rem, |a, b|{a % b}); + impl From for $T { fn from (value: f64) -> Self { Self(value.into()) } } + impl From for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } } + impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } } + impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } } + impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } } + impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } } + impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } } - impl TimeUnit for $T {} - impl_op!($T, Add, add, |a, b|{a + b}); - impl_op!($T, Sub, sub, |a, b|{a - b}); - impl_op!($T, Mul, mul, |a, b|{a * b}); - impl_op!($T, Div, div, |a, b|{a / b}); - impl_op!($T, Rem, rem, |a, b|{a % b}); - impl From for $T { fn from (value: f64) -> Self { Self(value.into()) } } - impl From for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } } - impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } } - impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } } - impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } } - impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } } - impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } -} } +impl std::fmt::Debug for Clock { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Clock") + .field("timebase", &self.timebase) + .field("chunk", &self.chunk) + .field("quant", &self.quant) + .field("sync", &self.sync) + .field("global", &self.global) + .field("playhead", &self.playhead) + .field("started", &self.started) + .finish() + } +} +impl Clock { + pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { + let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); + let timebase = Arc::new(Timebase::default()); + let clock = Self { + quant: Arc::new(24.into()), + sync: Arc::new(384.into()), + transport: Arc::new(Some(transport)), + chunk: Arc::new((chunk as usize).into()), + global: Arc::new(Moment::zero(&timebase)), + playhead: Arc::new(Moment::zero(&timebase)), + offset: Arc::new(Moment::zero(&timebase)), + started: RwLock::new(None).into(), + timebase, + midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), + midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), + click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), + ..Default::default() + }; + if let Some(bpm) = bpm { + clock.timebase.bpm.set(bpm); + } + Ok(clock) + } + pub fn timebase (&self) -> &Arc { + &self.timebase + } + /// Current sample rate + pub fn sr (&self) -> &SampleRate { + &self.timebase.sr + } + /// Current tempo + pub fn bpm (&self) -> &Bpm { + &self.timebase.bpm + } + /// Current MIDI resolution + pub fn ppq (&self) -> &Ppq { + &self.timebase.ppq + } + /// Next pulse that matches launch sync (for phrase switchover) + pub fn next_launch_pulse (&self) -> usize { + let sync = self.sync.get() as usize; + let pulse = self.playhead.pulse.get() as usize; + if pulse % sync == 0 { + pulse + } else { + (pulse / sync + 1) * sync + } + } + /// Start playing, optionally seeking to a given location beforehand + pub fn play_from (&self, start: Option) -> Usually<()> { + if let Some(transport) = self.transport.as_ref() { + if let Some(start) = start { + transport.locate(start)?; + } + transport.start()?; + } + Ok(()) + } + /// Pause, optionally seeking to a given location afterwards + pub fn pause_at (&self, pause: Option) -> Usually<()> { + if let Some(transport) = self.transport.as_ref() { + transport.stop()?; + if let Some(pause) = pause { + transport.locate(pause)?; + } + } + Ok(()) + } + /// Is currently paused? + pub fn is_stopped (&self) -> bool { + self.started.read().unwrap().is_none() + } + /// Is currently playing? + pub fn is_rolling (&self) -> bool { + self.started.read().unwrap().is_some() + } + /// Update chunk size + pub fn set_chunk (&self, n_frames: usize) { + self.chunk.store(n_frames, Relaxed); + } + pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { + // Store buffer length + self.set_chunk(scope.n_frames() as usize); + + // Store reported global frame and usec + let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; + self.global.sample.set(current_frames as f64); + self.global.usec.set(current_usecs as f64); + + let mut started = self.started.write().unwrap(); + + // If transport has just started or just stopped, + // update starting point: + if let Some(transport) = self.transport.as_ref() { + match (transport.query_state()?, started.as_ref()) { + (TransportState::Rolling, None) => { + let moment = Moment::zero(&self.timebase); + moment.sample.set(current_frames as f64); + moment.usec.set(current_usecs as f64); + *started = Some(moment); + }, + (TransportState::Stopped, Some(_)) => { + *started = None; + }, + _ => {} + }; + } + + self.playhead.update_from_sample(started.as_ref() + .map(|started|current_frames as f64 - started.sample.get()) + .unwrap_or(0.)); + + Ok(()) + } + + pub fn bbt (&self) -> PositionBBT { + let pulse = self.playhead.pulse.get() as i32; + let ppq = self.timebase.ppq.get() as i32; + let bpm = self.timebase.bpm.get(); + let bar = (pulse / ppq) / 4; + PositionBBT { + bar: 1 + bar, + beat: 1 + (pulse / ppq) % 4, + tick: (pulse % ppq), + bar_start_tick: (bar * 4 * ppq) as f64, + beat_type: 4., + beats_per_bar: 4., + beats_per_minute: bpm, + ticks_per_beat: ppq as f64 + } + } + + pub fn next_launch_instant (&self) -> Moment { + Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) + } + + /// Get index of first sample to populate. + /// + /// Greater than 0 means that the first pulse of the clip + /// falls somewhere in the middle of the chunk. + pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ + (scope.last_frame_time() as usize).saturating_sub( + started.sample.get() as usize + + self.started.read().unwrap().as_ref().unwrap().sample.get() as usize + ) + } + + // Get iterator that emits sample paired with pulse. + // + // * Sample: index into output buffer at which to write MIDI event + // * Pulse: index into clip from which to take the MIDI event + // + // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. + pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> Ticker { + self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) + } +} +impl Clock { + fn _todo_provide_u32 (&self) -> u32 { + todo!() + } + fn _todo_provide_opt_u32 (&self) -> Option { + todo!() + } + fn _todo_provide_f64 (&self) -> f64 { + todo!() + } +} +impl Command for ClockCommand { + fn execute (&self, state: &mut T) -> Perhaps { + self.execute(state.clock_mut()) // awesome + } +} +impl ClockView { + pub const BEAT_EMPTY: &'static str = "-.-.--"; + pub const TIME_EMPTY: &'static str = "-.---s"; + pub const BPM_EMPTY: &'static str = "---.---"; + pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { + let rate = clock.timebase.sr.get(); + let chunk = clock.chunk.load(Relaxed) as f64; + let lat = chunk / rate * 1000.; + let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); + let mut cache = cache.write().unwrap(); + cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); + cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); + cache.sr.update(Some((compact, rate)), |buf,_,_|{ + buf.clear(); + if compact { + write!(buf, "{:.1}kHz", rate / 1000.) + } else { + write!(buf, "{:.0}Hz", rate) + } + }); + if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { + let pulse = clock.timebase.usecs_to_pulse(now); + let time = now/1000000.; + let bpm = clock.timebase.bpm.get(); + cache.beat.update(Some(pulse), |buf, _, _|{ + buf.clear(); + clock.timebase.format_beats_1_to(buf, pulse) + }); + cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); + cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); + } else { + cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY)); + cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY)); + cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY)); + } + } +} +impl_default!(ClockView: { + let mut beat = String::with_capacity(16); + let _ = write!(beat, "{}", Self::BEAT_EMPTY); + let mut time = String::with_capacity(16); + let _ = write!(time, "{}", Self::TIME_EMPTY); + let mut bpm = String::with_capacity(16); + let _ = write!(bpm, "{}", Self::BPM_EMPTY); + Self { + beat: Memo::new(None, beat), + time: Memo::new(None, time), + bpm: Memo::new(None, bpm), + sr: Memo::new(None, String::with_capacity(16)), + buf: Memo::new(None, String::with_capacity(16)), + lat: Memo::new(None, String::with_capacity(16)), + } +}); + +#[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock); +impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ)); impl_time_unit!(SampleCount); impl_time_unit!(SampleRate); impl_time_unit!(Microsecond); @@ -460,243 +704,3 @@ impl_time_unit!(Ppq); impl_time_unit!(Pulse); impl_time_unit!(Bpm); impl_time_unit!(LaunchSync); - impl std::fmt::Debug for Clock { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Clock") - .field("timebase", &self.timebase) - .field("chunk", &self.chunk) - .field("quant", &self.quant) - .field("sync", &self.sync) - .field("global", &self.global) - .field("playhead", &self.playhead) - .field("started", &self.started) - .finish() - } - } - impl Clock { - pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { - let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); - let timebase = Arc::new(Timebase::default()); - let clock = Self { - quant: Arc::new(24.into()), - sync: Arc::new(384.into()), - transport: Arc::new(Some(transport)), - chunk: Arc::new((chunk as usize).into()), - global: Arc::new(Moment::zero(&timebase)), - playhead: Arc::new(Moment::zero(&timebase)), - offset: Arc::new(Moment::zero(&timebase)), - started: RwLock::new(None).into(), - timebase, - midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), - midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), - click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), - ..Default::default() - }; - if let Some(bpm) = bpm { - clock.timebase.bpm.set(bpm); - } - Ok(clock) - } - pub fn timebase (&self) -> &Arc { - &self.timebase - } - /// Current sample rate - pub fn sr (&self) -> &SampleRate { - &self.timebase.sr - } - /// Current tempo - pub fn bpm (&self) -> &Bpm { - &self.timebase.bpm - } - /// Current MIDI resolution - pub fn ppq (&self) -> &Ppq { - &self.timebase.ppq - } - /// Next pulse that matches launch sync (for phrase switchover) - pub fn next_launch_pulse (&self) -> usize { - let sync = self.sync.get() as usize; - let pulse = self.playhead.pulse.get() as usize; - if pulse % sync == 0 { - pulse - } else { - (pulse / sync + 1) * sync - } - } - /// Start playing, optionally seeking to a given location beforehand - pub fn play_from (&self, start: Option) -> Usually<()> { - if let Some(transport) = self.transport.as_ref() { - if let Some(start) = start { - transport.locate(start)?; - } - transport.start()?; - } - Ok(()) - } - /// Pause, optionally seeking to a given location afterwards - pub fn pause_at (&self, pause: Option) -> Usually<()> { - if let Some(transport) = self.transport.as_ref() { - transport.stop()?; - if let Some(pause) = pause { - transport.locate(pause)?; - } - } - Ok(()) - } - /// Is currently paused? - pub fn is_stopped (&self) -> bool { - self.started.read().unwrap().is_none() - } - /// Is currently playing? - pub fn is_rolling (&self) -> bool { - self.started.read().unwrap().is_some() - } - /// Update chunk size - pub fn set_chunk (&self, n_frames: usize) { - self.chunk.store(n_frames, Relaxed); - } - pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { - // Store buffer length - self.set_chunk(scope.n_frames() as usize); - - // Store reported global frame and usec - let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; - self.global.sample.set(current_frames as f64); - self.global.usec.set(current_usecs as f64); - - let mut started = self.started.write().unwrap(); - - // If transport has just started or just stopped, - // update starting point: - if let Some(transport) = self.transport.as_ref() { - match (transport.query_state()?, started.as_ref()) { - (TransportState::Rolling, None) => { - let moment = Moment::zero(&self.timebase); - moment.sample.set(current_frames as f64); - moment.usec.set(current_usecs as f64); - *started = Some(moment); - }, - (TransportState::Stopped, Some(_)) => { - *started = None; - }, - _ => {} - }; - } - - self.playhead.update_from_sample(started.as_ref() - .map(|started|current_frames as f64 - started.sample.get()) - .unwrap_or(0.)); - - Ok(()) - } - - pub fn bbt (&self) -> PositionBBT { - let pulse = self.playhead.pulse.get() as i32; - let ppq = self.timebase.ppq.get() as i32; - let bpm = self.timebase.bpm.get(); - let bar = (pulse / ppq) / 4; - PositionBBT { - bar: 1 + bar, - beat: 1 + (pulse / ppq) % 4, - tick: (pulse % ppq), - bar_start_tick: (bar * 4 * ppq) as f64, - beat_type: 4., - beats_per_bar: 4., - beats_per_minute: bpm, - ticks_per_beat: ppq as f64 - } - } - - pub fn next_launch_instant (&self) -> Moment { - Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) - } - - /// Get index of first sample to populate. - /// - /// Greater than 0 means that the first pulse of the clip - /// falls somewhere in the middle of the chunk. - pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ - (scope.last_frame_time() as usize).saturating_sub( - started.sample.get() as usize + - self.started.read().unwrap().as_ref().unwrap().sample.get() as usize - ) - } - - // Get iterator that emits sample paired with pulse. - // - // * Sample: index into output buffer at which to write MIDI event - // * Pulse: index into clip from which to take the MIDI event - // - // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. - pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> Ticker { - self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) - } - } - impl Clock { - fn _todo_provide_u32 (&self) -> u32 { - todo!() - } - fn _todo_provide_opt_u32 (&self) -> Option { - todo!() - } - fn _todo_provide_f64 (&self) -> f64 { - todo!() - } - } - impl Command for ClockCommand { - fn execute (&self, state: &mut T) -> Perhaps { - self.execute(state.clock_mut()) // awesome - } - } - impl ClockView { - pub const BEAT_EMPTY: &'static str = "-.-.--"; - pub const TIME_EMPTY: &'static str = "-.---s"; - pub const BPM_EMPTY: &'static str = "---.---"; - pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { - let rate = clock.timebase.sr.get(); - let chunk = clock.chunk.load(Relaxed) as f64; - let lat = chunk / rate * 1000.; - let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); - let mut cache = cache.write().unwrap(); - cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); - cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); - cache.sr.update(Some((compact, rate)), |buf,_,_|{ - buf.clear(); - if compact { - write!(buf, "{:.1}kHz", rate / 1000.) - } else { - write!(buf, "{:.0}Hz", rate) - } - }); - if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { - let pulse = clock.timebase.usecs_to_pulse(now); - let time = now/1000000.; - let bpm = clock.timebase.bpm.get(); - cache.beat.update(Some(pulse), |buf, _, _|{ - buf.clear(); - clock.timebase.format_beats_1_to(buf, pulse) - }); - cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); - cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); - } else { - cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY)); - cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY)); - cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY)); - } - } - } - impl_default!(ClockView: { - let mut beat = String::with_capacity(16); - let _ = write!(beat, "{}", Self::BEAT_EMPTY); - let mut time = String::with_capacity(16); - let _ = write!(time, "{}", Self::TIME_EMPTY); - let mut bpm = String::with_capacity(16); - let _ = write!(bpm, "{}", Self::BPM_EMPTY); - Self { - beat: Memo::new(None, beat), - time: Memo::new(None, time), - bpm: Memo::new(None, bpm), - sr: Memo::new(None, String::with_capacity(16)), - buf: Memo::new(None, String::with_capacity(16)), - lat: Memo::new(None, String::with_capacity(16)), - } - }); diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 00000000..d550e789 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,92 @@ +use crate::*; + +/// Configuration: mode, view, and bind definitions. +/// +/// ``` +/// let config = tek::Config::default(); +/// ``` +/// +/// ``` +/// // Some dizzle. +/// // What indentation to use here lol? +/// let source = stringify!((mode :menu (name Menu) +/// (info Mode selector.) (keys :axis/y :confirm) +/// (view (bg (g 0) (bsp/s :ports/out +/// (bsp/n :ports/in +/// (bg (g 30) (bsp/s (fixed/y 7 :logo) +/// (fill :dialog/menu))))))))); +/// // Add this definition to the config and try to load it. +/// // A "mode" is basically a state machine +/// // with associated input and output definitions. +/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap(); +/// ``` +#[derive(Default, Debug)] pub struct Config { + /// XDG base directories of running user. + pub dirs: BaseDirectories, + /// Active collection of interaction modes. + pub modes: Modes, + /// Active collection of event bindings. + pub binds: Binds, + /// Active collection of view definitions. + pub views: Views, +} +impl Config { + const CONFIG_DIR: &'static str = "tek"; + const CONFIG_SUB: &'static str = "v0"; + const CONFIG: &'static str = "tek.edn"; + const DEFAULTS: &'static str = include_str!("./tek.edn"); + /// Create a new app configuration from a set of XDG base directories, + pub fn new (dirs: Option) -> Self { + let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); + let dirs = dirs.unwrap_or_else(default); + Self { dirs, ..Default::default() } + } + /// Write initial contents of configuration. + pub fn init (&mut self) -> Usually<()> { + self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ + cfgs.add(&dsl)?; + Ok(()) + })?; + Ok(()) + } + /// Write initial contents of a configuration file. + pub fn init_one ( + &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> + ) -> Usually<()> { + if self.dirs.find_config_file(path).is_none() { + //println!("Creating {path:?}"); + std::fs::write(self.dirs.place_config_file(path)?, defaults)?; + } + Ok(if let Some(path) = self.dirs.find_config_file(path) { + //println!("Loading {path:?}"); + let src = std::fs::read_to_string(&path)?; + src.as_str().each(move|item|each(self, item))?; + } else { + return Err(format!("{path}: not found").into()) + }) + } + /// Add statements to configuration from [Dsl] source. + pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { + dsl.each(|item|self.add_one(item))?; + Ok(self) + } + fn add_one (&self, item: impl Language) -> Usually<()> { + if let Some(expr) = item.expr()? { + let head = expr.head()?; + let tail = expr.tail()?; + let name = tail.head()?; + let body = tail.tail()?; + //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); + match head { + Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, + Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, + Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, + _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) + } + Ok(()) + } else { + return Err(format!("Config::load: expected expr, got: {item:?}").into()) + } + } +} + diff --git a/src/device.rs b/src/device.rs index bd44bf77..f914f8bc 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,3 +1,26 @@ +use crate::*; + +def_command!(DeviceCommand: |device: Device| {}); + +def_command!(MidiInputCommand: |port: MidiInput| { + Close => todo!(), + Connect { midi_out: Arc } => todo!(), +}); + +def_command!(MidiOutputCommand: |port: MidiOutput| { + Close => todo!(), + Connect { midi_in: Arc } => todo!(), +}); + +def_command!(AudioInputCommand: |port: AudioInput| { + Close => todo!(), + Connect { audio_out: Arc } => todo!(), +}); + +def_command!(AudioOutputCommand: |port: AudioOutput| { + Close => todo!(), + Connect { audio_in: Arc } => todo!(), +}); impl Device { pub fn name (&self) -> &str { @@ -581,3 +604,19 @@ impl_audio!(|self: DeviceAudio<'a>, client, scope|{ #[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO } }); + +pub fn device_kinds () -> &'static [&'static str] { + &[ + #[cfg(feature = "sampler")] "Sampler", + #[cfg(feature = "lv2")] "Plugin (LV2)", + ] +} + +impl> + AsMut>> HasDevices for T { + fn devices (&self) -> &Vec { + self.as_ref() + } + fn devices_mut (&mut self) -> &mut Vec { + self.as_mut() + } +} diff --git a/src/dialog.rs b/src/dialog.rs new file mode 100644 index 00000000..61baec11 --- /dev/null +++ b/src/dialog.rs @@ -0,0 +1,106 @@ +use crate::*; + +/// Various possible dialog modes. +/// +/// ``` +/// let dialog: tek::Dialog = Default::default(); +/// ``` +#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { + #[default] None, + Help(usize), + Menu(usize, MenuItems), + Device(usize), + Message(Arc), + Browse(BrowseTarget, Arc), + Options, +} +namespace!(App: Dialog { symbol = |app| { + ":dialog/none" => Dialog::None, + ":dialog/options" => Dialog::Options, + ":dialog/device" => Dialog::Device(0), + ":dialog/device/prev" => Dialog::Device(0), + ":dialog/device/next" => Dialog::Device(0), + ":dialog/help" => Dialog::Help(0), + ":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, + Browse::new(None).unwrap().into()), + ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, + Browse::new(None).unwrap().into()), + ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), + Browse::new(None).unwrap().into()), +}; }); +impl Dialog { + /// ``` + /// let _ = tek::Dialog::welcome(); + /// ``` + pub fn welcome () -> Self { + Self::Menu(1, MenuItems([ + MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), + MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ + app.dialog = Dialog::None; + app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); + })))), + MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), + ].into())) + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().menu_selected(); + /// ``` + pub fn menu_selected (&self) -> Option { + if let Self::Menu(selected, _) = self { Some(*selected) } else { None } + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().menu_next(); + /// ``` + pub fn menu_next (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().menu_prev(); + /// ``` + pub fn menu_prev (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().device_kind(); + /// ``` + pub fn device_kind (&self) -> Option { + if let Self::Device(index) = self { Some(*index) } else { None } + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().device_kind_next(); + /// ``` + pub fn device_kind_next (&self) -> Option { + self.device_kind().map(|index|(index + 1) % device_kinds().len()) + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().device_kind_prev(); + /// ``` + pub fn device_kind_prev (&self) -> Option { + self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) + } + /// FIXME: implement + pub fn message (&self) -> Option<&str> { todo!() } + /// FIXME: implement + pub fn browser (&self) -> Option<&Arc> { todo!() } + /// FIXME: implement + pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } +} + diff --git a/src/menu.rs b/src/menu.rs new file mode 100644 index 00000000..8e1cb5f2 --- /dev/null +++ b/src/menu.rs @@ -0,0 +1,27 @@ +use crate::*; + +impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); +impl_default!(MenuItem: Self("".into(), Arc::new(Box::new(|_|Ok(()))))); +impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } +impl AsRef> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } } + +/// List of menu items. +/// +/// ``` +/// let items: tek::MenuItems = Default::default(); +/// ``` +#[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems( + pub Arc<[MenuItem]> +); + +/// An item of a menu. +/// +/// ``` +/// let item: tek::MenuItem = Default::default(); +/// ``` +#[derive(Clone)] pub struct MenuItem( + /// Label + pub Arc, + /// Callback + pub ArcUsually<()> + Send + Sync>> +); diff --git a/src/mix.rs b/src/mix.rs index 4e3a0ec2..10a44361 100644 --- a/src/mix.rs +++ b/src/mix.rs @@ -1,3 +1,4 @@ +use crate::*; #[derive(Debug, Default)] pub enum MeteringMode { #[default] Rms, Log10, @@ -68,3 +69,53 @@ h_full(RmsMeter(*value)) }))) } + +pub fn mix_summing ( + buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, +) -> bool { + let channels = buffer.len(); + for index in 0..frames { + if let Some(frame) = next() { + for (channel, sample) in frame.iter().enumerate() { + let channel = channel % channels; + buffer[channel][index] += sample * gain; + } + } else { + return false + } + } + true +} + +pub fn mix_average ( + buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, +) -> bool { + let channels = buffer.len(); + for index in 0..frames { + if let Some(frame) = next() { + for (channel, sample) in frame.iter().enumerate() { + let channel = channel % channels; + let value = buffer[channel][index]; + buffer[channel][index] = (value + sample * gain) / 2.0; + } + } else { + return false + } + } + true +} + +pub fn to_log10 (samples: &[f32]) -> f32 { + let total: f32 = samples.iter().map(|x|x.abs()).sum(); + let count = samples.len() as f32; + 10. * (total / count).log10() +} + + +pub fn to_rms (samples: &[f32]) -> f32 { + let sum = samples.iter() + .map(|s|*s) + .reduce(|sum, sample|sum + sample.abs()) + .unwrap_or(0.0); + (sum / samples.len() as f32).sqrt() +} diff --git a/src/mode.rs b/src/mode.rs new file mode 100644 index 00000000..46692c39 --- /dev/null +++ b/src/mode.rs @@ -0,0 +1,98 @@ +use crate::*; +impl Config { + pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { + self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() + } +} + +pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef, body: &impl Language) -> Usually<()> { + let mut mode = Mode::default(); + body.each(|item|mode.add(item))?; + modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); + Ok(()) +} + +/// Collection of interaction modes. +pub type Modes = Arc, Arc>>>>>; + +impl Mode> { + /// Add a definition to the mode. + /// + /// Supported definitions: + /// + /// - (name ...) -> name + /// - (info ...) -> description + /// - (keys ...) -> key bindings + /// - (mode ...) -> submode + /// - ... -> view + /// + /// ``` + /// let mut mode: tek::Mode> = Default::default(); + /// mode.add("(name hello)").unwrap(); + /// ``` + pub fn add (&mut self, dsl: impl Language) -> Usually<()> { + Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { + //println!("Mode::add: {head} {:?}", expr.tail()); + let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + match head { + "name" => self.add_name(tail)?, + "info" => self.add_info(tail)?, + "keys" => self.add_keys(tail)?, + "mode" => self.add_mode(tail)?, + _ => self.add_view(tail)?, + }; + } else if let Ok(Some(word)) = dsl.word() { + self.add_view(word); + } else { + return Err(format!("Mode::add: unexpected: {dsl:?}").into()); + }) + + //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) + //.word(|word|self.add_view(word)) + //.expr(|expr|expr.head(|head|{ + ////println!("Mode::add: {head} {:?}", expr.tail()); + //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + //match head { + //"name" => self.add_name(tail), + //"info" => self.add_info(tail), + //"keys" => self.add_keys(tail)?, + //"mode" => self.add_mode(tail)?, + //_ => self.add_view(tail), + //}; + //})) + } + + fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.name.push(src.into()))) + } + fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.info.push(src.into()))) + } + fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.view.push(src.into()))) + } + fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) + } + fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(if let Some(id) = dsl.head()? { + load_mode(&self.modes, &id, &dsl.tail())?; + } else { + return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); + })) + } +} +/// Group of view and keys definitions. +/// +/// ``` +/// let mode = tek::Mode::>::default(); +/// ``` +#[derive(Default, Debug)] pub struct Mode { + pub path: PathBuf, + pub name: Vec, + pub info: Vec, + pub view: Vec, + pub keys: Vec, + pub modes: Modes, +} + diff --git a/src/plugin.rs b/src/plugin.rs index 13d9aae8..0c233240 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,3 +1,4 @@ +use crate::*; /// A LV2 plugin. #[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 { @@ -176,3 +177,30 @@ fn draw_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) { } #[cfg(feature = "vst2")] impl ::vst::host::Host for Plugin {} + + +#[cfg(feature = "vst2")] fn set_vst_plugin ( + host: &Arc>>, _path: &str +) -> Usually { + let mut loader = ::vst::host::PluginLoader::load( + &std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"), + host.clone() + )?; + Ok(PluginKind::VST2 { + instance: loader.instance()? + }) +} + +#[cfg(feature = "lv2_gui")] +pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually> { + Ok(spawn(move||{ + let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap(); + event_loop.set_control_flow(ControlFlow::Wait); + event_loop.run_app(&mut ui).unwrap() + })) +} + +#[cfg(feature = "lv2_gui")] +fn lv2_ui_instantiate (kind: &str) { + //let host = Suil +} diff --git a/src/sample.rs b/src/sample.rs index 74fba95c..4dc65344 100644 --- a/src/sample.rs +++ b/src/sample.rs @@ -1,6 +1,54 @@ +use crate::*; use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput}; +def_command!(SamplerCommand: |sampler: Sampler| { + RecordToggle { slot: usize } => { + let slot = *slot; + let recording = sampler.recording.as_ref().map(|x|x.0); + let _ = Self::RecordFinish.execute(sampler)?; + // autoslice: continue recording at next slot + if recording != Some(slot) { + Self::RecordBegin { slot }.execute(sampler) + } else { + Ok(None) + } + }, + RecordBegin { slot: usize } => { + let slot = *slot; + sampler.recording = Some(( + slot, + Some(Arc::new(RwLock::new(Sample::new( + "Sample", 0, 0, vec![vec![];sampler.audio_ins.len()] + )))) + )); + Ok(None) + }, + RecordFinish => { + let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{ + std::mem::swap(sample, &mut sampler.samples.0[*index]); + sample + }); // TODO: undo + Ok(None) + }, + RecordCancel => { + sampler.recording = None; + Ok(None) + }, + PlaySample { slot: usize } => { + let slot = *slot; + if let Some(ref sample) = sampler.samples.0[slot] { + sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128))); + } + Ok(None) + }, + StopSample { slot: usize } => { + let _slot = *slot; + todo!(); + //Ok(None) + }, +}); + /// Plays [Voice]s from [Sample]s. /// /// ``` @@ -651,3 +699,7 @@ fn draw_sample ( to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); Ok(label1.len() + label2.len() + 4) } + +fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { + todo!(); +} diff --git a/src/sequence.rs b/src/sequence.rs index 1e55f0dc..e697290b 100644 --- a/src/sequence.rs +++ b/src/sequence.rs @@ -1,5 +1,30 @@ +use crate::*; use ::std::sync::{Arc, RwLock}; +def_command!(MidiEditCommand: |editor: MidiEditor| { + Show { clip: Option>> } => { + editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) }, + DeleteNote => { + editor.redraw(); todo!() }, + AppendNote { advance: bool } => { + editor.put_note(*advance); editor.redraw(); Ok(None) }, + SetNotePos { pos: usize } => { + editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) }, + SetNoteLen { len: usize } => { + editor.set_note_len(*len); editor.redraw(); Ok(None) }, + SetNoteScroll { scroll: usize } => { + editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) }, + SetTimePos { pos: usize } => { + editor.set_time_pos(*pos); editor.redraw(); Ok(None) }, + SetTimeScroll { scroll: usize } => { + editor.set_time_start(*scroll); editor.redraw(); Ok(None) }, + SetTimeZoom { zoom: usize } => { + editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) }, + SetTimeLock { lock: bool } => { + editor.set_time_lock(*lock); editor.redraw(); Ok(None) }, + // TODO: 1-9 seek markers that by default start every 8th of the clip +}); + /// Contains state for viewing and editing a clip. /// /// ``` @@ -1414,3 +1439,70 @@ impl Iterator for Ticker { } } } + +fn to_key (note: usize) -> &'static str { + match note % 12 { + 11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌", + 10 | 8 | 6 | 3 | 1 => " ", + _ => unreachable!(), + } +} + +pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) + -> impl Iterator +{ + (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) +} + +/// Return boxed iterator of MIDI events +pub fn parse_midi_input <'a> (input: ::tengri::jack::MidiIter<'a>) + -> Box, &'a [u8])> + 'a> +{ + Box::new(input.map(|::tengri::jack::RawMidi { time, bytes }|( + time as usize, + LiveEvent::parse(bytes).unwrap(), + bytes + ))) +} + +/// Add "all notes off" to the start of a buffer. +pub fn all_notes_off (output: &mut [Vec>]) { + let mut buf = vec![]; + let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; + let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; + evt.write(&mut buf).unwrap(); + output[0].push(buf); +} + +/// Update notes_in array +pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { + match message { + MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } + MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, + _ => {} + } +} + +/// Returns the next shorter length +pub fn note_duration_prev (pulses: usize) -> usize { + for (length, _) in NOTE_DURATIONS.iter().rev() { if *length < pulses { return *length } } + pulses +} + +/// Returns the next longer length +pub fn note_duration_next (pulses: usize) -> usize { + for (length, _) in NOTE_DURATIONS.iter() { if *length > pulses { return *length } } + pulses +} + +pub fn note_duration_to_name (pulses: usize) -> &'static str { + for (length, name) in NOTE_DURATIONS.iter() { if *length == pulses { return name } } + "" +} + +pub fn note_pitch_to_name (n: usize) -> &'static str { + if n > 127 { + panic!("to_note_name({n}): must be 0-127"); + } + NOTE_NAMES[n] +} diff --git a/src/tek.rs b/src/tek.rs index 8e04a885..838c8db4 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -39,13 +39,20 @@ } pub mod arrange; +pub mod bind; pub mod browse; +pub mod cli; pub mod clock; +pub mod config; pub mod device; +pub mod dialog; +pub mod menu; pub mod mix; +pub mod mode; pub mod plugin; pub mod sample; pub mod sequence; +pub mod view; use clap::{self, Parser, Subcommand}; use builder_pattern::Builder; @@ -184,23 +191,6 @@ fn tek_dec (state: &mut App, axis: &ControlAxis) -> Perhaps { }) } -pub(crate) fn load_view (views: &Views, name: &impl AsRef, body: &impl Language) -> Usually<()> { - views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into()); - Ok(()) -} - -pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef, body: &impl Language) -> Usually<()> { - let mut mode = Mode::default(); - body.each(|item|mode.add(item))?; - modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); - Ok(()) -} - -pub(crate) fn load_bind (binds: &Binds, name: &impl AsRef, body: &impl Language) -> Usually<()> { - binds.write().unwrap().insert(name.as_ref().into(), Bind::load(body)?); - Ok(()) -} - fn collect_commands (app: &App, input: &TuiIn) -> Usually> { let mut commands = vec![]; for id in app.mode.keys.iter() { @@ -277,146 +267,6 @@ pub fn tek_jack_event (app: &mut App, event: JackEvent) { } } -pub fn tek_show_version () { - println!("todo version"); -} - -pub fn tek_print_config (config: &Config) { - use ::ansi_term::Color::*; - println!("{:?}", config.dirs); - for (k, v) in config.views.read().unwrap().iter() { - println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}"))); - } - for (k, v) in config.binds.read().unwrap().iter() { - println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}"))); - for (k, v) in v.0.iter() { - print!("{} ", &Yellow.paint(match &k.0 { - Event::Key(KeyEvent { modifiers, .. }) => - format!("{:>16}", format!("{modifiers}")), - _ => unimplemented!() - })); - print!("{}", &Yellow.bold().paint(match &k.0 { - Event::Key(KeyEvent { code, .. }) => - format!("{:<10}", format!("{code}")), - _ => unimplemented!() - })); - for v in v.iter() { - print!(" => {:?}", v.commands); - print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default()); - println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default()); - //println!(" {:?}", v.source); - } - } - } - for (k, v) in config.modes.read().unwrap().iter() { - println!(); - for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); } - for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); } - print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}"))); - print!("\n{}", Blue.paint("KEYS")); - for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); } - println!(); - for (k, v) in v.modes.read().unwrap().iter() { - print!("{} {} {:?}", - Blue.paint("MODE"), - Green.bold().paint(format!("{k:<16}")), - v.name); - print!(" INFO={:?}", - v.info); - print!(" VIEW={:?}", - v.view); - println!(" KEYS={:?}", - v.keys); - } - print!("{}", Blue.paint("VIEW")); - for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); } - println!(); - } -} - -pub fn tek_print_status (project: &Arrangement) { - println!("Name: {:?}", &project.name); - println!("JACK: {:?}", &project.jack); - println!("Buffer: {:?}", &project.clock.chunk); - println!("Sample rate: {:?}", &project.clock.timebase.sr); - println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); - println!("Tempo: {:?}", &project.clock.timebase.bpm); - println!("Quantize: {:?}", &project.clock.quant); - println!("Launch: {:?}", &project.clock.sync); - println!("Playhead: {:?}us", &project.clock.playhead.usec); - println!("Playhead: {:?}s", &project.clock.playhead.sample); - println!("Playhead: {:?}p", &project.clock.playhead.pulse); - println!("Started: {:?}", &project.clock.started); - println!("Tracks:"); - for (i, t) in project.tracks.iter().enumerate() { - println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, - &t.sequencer.play_clip, &t.sequencer.next_clip); - } - println!("Scenes:"); - for (i, t) in project.scenes.iter().enumerate() { - println!(" Scene {i}: {} {:?}", &t.name, &t.clips); - } - println!("MIDI Ins: {:?}", &project.midi_ins); - println!("MIDI Outs: {:?}", &project.midi_outs); - println!("Audio Ins: {:?}", &project.audio_ins); - println!("Audio Outs: {:?}", &project.audio_outs); - // TODO git integration - // TODO dawvert integration -} - -/// Return boxed iterator of MIDI events -pub fn parse_midi_input <'a> (input: ::tengri::jack::MidiIter<'a>) - -> Box, &'a [u8])> + 'a> -{ - Box::new(input.map(|::tengri::jack::RawMidi { time, bytes }|( - time as usize, - LiveEvent::parse(bytes).unwrap(), - bytes - ))) -} - -/// Add "all notes off" to the start of a buffer. -pub fn all_notes_off (output: &mut [Vec>]) { - let mut buf = vec![]; - let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; - let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; - evt.write(&mut buf).unwrap(); - output[0].push(buf); -} - -/// Update notes_in array -pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { - match message { - MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } - MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, - _ => {} - } -} - -/// Returns the next shorter length -pub fn note_duration_prev (pulses: usize) -> usize { - for (length, _) in NOTE_DURATIONS.iter().rev() { if *length < pulses { return *length } } - pulses -} - -/// Returns the next longer length -pub fn note_duration_next (pulses: usize) -> usize { - for (length, _) in NOTE_DURATIONS.iter() { if *length > pulses { return *length } } - pulses -} - -pub fn note_duration_to_name (pulses: usize) -> &'static str { - for (length, name) in NOTE_DURATIONS.iter() { if *length == pulses { return name } } - "" -} - -pub fn note_pitch_to_name (n: usize) -> &'static str { - if n > 127 { - panic!("to_note_name({n}): must be 0-127"); - } - NOTE_NAMES[n] -} - pub fn swap_value ( target: &mut T, value: &T, returned: impl Fn(T)->U ) -> Perhaps { @@ -441,22 +291,6 @@ pub fn toggle_bool ( } } -pub fn device_kinds () -> &'static [&'static str] { - &[ - #[cfg(feature = "sampler")] "Sampler", - #[cfg(feature = "lv2")] "Plugin (LV2)", - ] -} - -impl> + AsMut>> HasDevices for T { - fn devices (&self) -> &Vec { - self.as_ref() - } - fn devices_mut (&mut self) -> &mut Vec { - self.as_mut() - } -} - //take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); @@ -469,90 +303,6 @@ impl> + AsMut>> HasDevices for T { } } -fn to_key (note: usize) -> &'static str { - match note % 12 { - 11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌", - 10 | 8 | 6 | 3 | 1 => " ", - _ => unreachable!(), - } -} - -pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) - -> impl Iterator -{ - (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) -} - - -#[cfg(feature = "lv2_gui")] -pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually> { - Ok(spawn(move||{ - let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap(); - event_loop.set_control_flow(ControlFlow::Wait); - event_loop.run_app(&mut ui).unwrap() - })) -} - -#[cfg(feature = "lv2_gui")] -fn lv2_ui_instantiate (kind: &str) { - //let host = Suil -} - -pub fn mix_summing ( - buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, -) -> bool { - let channels = buffer.len(); - for index in 0..frames { - if let Some(frame) = next() { - for (channel, sample) in frame.iter().enumerate() { - let channel = channel % channels; - buffer[channel][index] += sample * gain; - } - } else { - return false - } - } - true -} - -pub fn mix_average ( - buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, -) -> bool { - let channels = buffer.len(); - for index in 0..frames { - if let Some(frame) = next() { - for (channel, sample) in frame.iter().enumerate() { - let channel = channel % channels; - let value = buffer[channel][index]; - buffer[channel][index] = (value + sample * gain) / 2.0; - } - } else { - return false - } - } - true -} - -pub fn to_log10 (samples: &[f32]) -> f32 { - let total: f32 = samples.iter().map(|x|x.abs()).sum(); - let count = samples.len() as f32; - 10. * (total / count).log10() -} - - -pub fn to_rms (samples: &[f32]) -> f32 { - let sum = samples.iter() - .map(|s|*s) - .reduce(|sum, sample|sum + sample.abs()) - .unwrap_or(0.0); - (sum / samples.len() as f32).sqrt() -} - - -fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { - todo!(); -} - fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { let (mut subdirs, mut files) = std::fs::read_dir(dir)? .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ @@ -570,25 +320,10 @@ fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { Ok((subdirs, files)) } - - pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { track.width as u16 } - -#[cfg(feature = "vst2")] fn set_vst_plugin ( - host: &Arc>>, _path: &str -) -> Usually { - let mut loader = ::vst::host::PluginLoader::load( - &std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"), - host.clone() - )?; - Ok(PluginKind::VST2 { - instance: loader.instance()? - }) -} - def_command!(AppCommand: |app: App| { Nop => Ok(None), Confirm => tek_confirm(app), @@ -600,317 +335,6 @@ def_command!(AppCommand: |app: App| { }, }); -def_command!(DeviceCommand: |device: Device| {}); - -def_command!(ClipCommand: |clip: MidiClip| { - SetColor { color: Option } => { - //(SetColor [t: usize, s: usize, c: ItemTheme] - //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); - //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) - todo!() - }, - SetLoop { looping: Option } => { - //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) - //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) - todo!() - } -}); - -def_command!(BrowseCommand: |browse: Browse| { - SetVisible => Ok(None), - SetPath { address: PathBuf } => Ok(None), - SetSearch { filter: Arc } => Ok(None), - SetCursor { cursor: usize } => Ok(None), -}); - -def_command!(MidiEditCommand: |editor: MidiEditor| { - Show { clip: Option>> } => { - editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) }, - DeleteNote => { - editor.redraw(); todo!() }, - AppendNote { advance: bool } => { - editor.put_note(*advance); editor.redraw(); Ok(None) }, - SetNotePos { pos: usize } => { - editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) }, - SetNoteLen { len: usize } => { - editor.set_note_len(*len); editor.redraw(); Ok(None) }, - SetNoteScroll { scroll: usize } => { - editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) }, - SetTimePos { pos: usize } => { - editor.set_time_pos(*pos); editor.redraw(); Ok(None) }, - SetTimeScroll { scroll: usize } => { - editor.set_time_start(*scroll); editor.redraw(); Ok(None) }, - SetTimeZoom { zoom: usize } => { - editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) }, - SetTimeLock { lock: bool } => { - editor.set_time_lock(*lock); editor.redraw(); Ok(None) }, - // TODO: 1-9 seek markers that by default start every 8th of the clip -}); - -def_command!(PoolCommand: |pool: Pool| { - // Toggle visibility of pool - Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) }, - // Select a clip from the clip pool - Select { index: usize } => { pool.set_clip_index(*index); Ok(None) }, - // Update the contents of the clip pool - Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})), - // Rename a clip - Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?), - // Change the length of a clip - Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?), - // Import from file - Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { - command.delegate(browse, |command|Self::Import{command})? - } else { - None - }), - // Export to file - Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { - command.delegate(browse, |command|Self::Export{command})? - } else { - None - }), -}); - -def_command!(PoolClipCommand: |pool: Pool| { - Delete { index: usize } => { - let index = *index; - let clip = pool.clips_mut().remove(index).read().unwrap().clone(); - Ok(Some(Self::Add { index, clip })) - }, - Swap { index: usize, other: usize } => { - let index = *index; - let other = *other; - pool.clips_mut().swap(index, other); - Ok(Some(Self::Swap { index, other })) - }, - Export { index: usize, path: PathBuf } => { - todo!("export clip to midi file"); - }, - Add { index: usize, clip: MidiClip } => { - let index = *index; - let mut index = index; - let clip = Arc::new(RwLock::new(clip.clone())); - let mut clips = pool.clips_mut(); - if index >= clips.len() { - index = clips.len(); - clips.push(clip) - } else { - clips.insert(index, clip); - } - Ok(Some(Self::Delete { index })) - }, - Import { index: usize, path: PathBuf } => { - let index = *index; - let bytes = std::fs::read(&path)?; - let smf = Smf::parse(bytes.as_slice())?; - let mut t = 0u32; - let mut events = vec![]; - for track in smf.tracks.iter() { - for event in track.iter() { - t += event.delta.as_int(); - if let TrackEventKind::Midi { channel, message } = event.kind { - events.push((t, channel.as_int(), message)); - } - } - } - let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None); - for event in events.iter() { - clip.notes[event.0 as usize].push(event.2); - } - Ok(Self::Add { index, clip }.execute(pool)?) - }, - SetName { index: usize, name: Arc } => { - let index = *index; - let clip = &mut pool.clips_mut()[index]; - let old_name = clip.read().unwrap().name.clone(); - clip.write().unwrap().name = name.clone(); - Ok(Some(Self::SetName { index, name: old_name })) - }, - SetLength { index: usize, length: usize } => { - let index = *index; - let clip = &mut pool.clips_mut()[index]; - let old_len = clip.read().unwrap().length; - clip.write().unwrap().length = *length; - Ok(Some(Self::SetLength { index, length: old_len })) - }, - SetColor { index: usize, color: ItemColor } => { - let index = *index; - let mut color = ItemTheme::from(*color); - std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color); - Ok(Some(Self::SetColor { index, color: color.base })) - }, -}); - -def_command!(RenameCommand: |pool: Pool| { - Begin => unreachable!(), - Cancel => { - if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { - pool.clips()[clip].write().unwrap().name = old_name.clone().into(); - } - Ok(None) - }, - Confirm => { - if let Some(PoolMode::Rename(_clip, ref mut old_name)) = pool.mode_mut().clone() { - let old_name = old_name.clone(); *pool.mode_mut() = None; return Ok(Some(Self::Set { value: old_name })) - } - Ok(None) - }, - Set { value: Arc } => { - if let Some(PoolMode::Rename(clip, ref mut _old_name)) = pool.mode_mut().clone() { - pool.clips()[clip].write().unwrap().name = value.clone(); - } - Ok(None) - }, -}); - -def_command!(CropCommand: |pool: Pool| { - Begin => unreachable!(), - Cancel => { if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { *pool.mode_mut() = None; } Ok(None) }, - Set { length: usize } => { - if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus)) - = pool.mode_mut().clone() - { - let old_length; - { - let clip = pool.clips()[clip].clone();//.write().unwrap(); - old_length = Some(clip.read().unwrap().length); - clip.write().unwrap().length = *length; - } - *pool.mode_mut() = None; - return Ok(old_length.map(|length|Self::Set { length })) - } - Ok(None) - }, - Next => { - if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.next() }; Ok(None) - }, - Prev => { - if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.prev() }; Ok(None) - }, - Inc => { - if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() { - match focus { - ClipLengthFocus::Bar => { *length += 4 * PPQ }, - ClipLengthFocus::Beat => { *length += PPQ }, - ClipLengthFocus::Tick => { *length += 1 }, - } - } - Ok(None) - }, - Dec => { - if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() { - match focus { - ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) }, - ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) }, - ClipLengthFocus::Tick => { *length = length.saturating_sub(1) }, - } - } - Ok(None) - } -}); - -def_command!(MidiInputCommand: |port: MidiInput| { - Close => todo!(), - Connect { midi_out: Arc } => todo!(), -}); - -def_command!(MidiOutputCommand: |port: MidiOutput| { - Close => todo!(), - Connect { midi_in: Arc } => todo!(), -}); - -def_command!(AudioInputCommand: |port: AudioInput| { - Close => todo!(), - Connect { audio_out: Arc } => todo!(), -}); - -def_command!(AudioOutputCommand: |port: AudioOutput| { - Close => todo!(), - Connect { audio_in: Arc } => todo!(), -}); - -def_command!(SamplerCommand: |sampler: Sampler| { - RecordToggle { slot: usize } => { - let slot = *slot; - let recording = sampler.recording.as_ref().map(|x|x.0); - let _ = Self::RecordFinish.execute(sampler)?; - // autoslice: continue recording at next slot - if recording != Some(slot) { - Self::RecordBegin { slot }.execute(sampler) - } else { - Ok(None) - } - }, - RecordBegin { slot: usize } => { - let slot = *slot; - sampler.recording = Some(( - slot, - Some(Arc::new(RwLock::new(Sample::new( - "Sample", 0, 0, vec![vec![];sampler.audio_ins.len()] - )))) - )); - Ok(None) - }, - RecordFinish => { - let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{ - std::mem::swap(sample, &mut sampler.samples.0[*index]); - sample - }); // TODO: undo - Ok(None) - }, - RecordCancel => { - sampler.recording = None; - Ok(None) - }, - PlaySample { slot: usize } => { - let slot = *slot; - if let Some(ref sample) = sampler.samples.0[slot] { - sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128))); - } - Ok(None) - }, - StopSample { slot: usize } => { - let _slot = *slot; - todo!(); - //Ok(None) - }, -}); - -def_command!(FileBrowserCommand: |sampler: Sampler|{ - //("begin" [] Some(Self::Begin)) - //("cancel" [] Some(Self::Cancel)) - //("confirm" [] Some(Self::Confirm)) - //("select" [i: usize] Some(Self::Select(i.expect("no index")))) - //("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path")))) - //("filter" [f: Arc] Some(Self::Filter(f.expect("no filter"))))) -}); - -def_command!(SceneCommand: |scene: Scene| { - SetSize { size: usize } => { todo!() }, - SetZoom { size: usize } => { todo!() }, - SetName { name: Arc } => - swap_value(&mut scene.name, name, |name|Self::SetName{name}), - SetColor { color: ItemTheme } => - swap_value(&mut scene.color, color, |color|Self::SetColor{color}), -}); - -def_command!(TrackCommand: |track: Track| { - Stop => { track.sequencer.enqueue_next(None); Ok(None) }, - SetMute { mute: Option } => todo!(), - SetSolo { solo: Option } => todo!(), - SetSize { size: usize } => todo!(), - SetZoom { zoom: usize } => todo!(), - SetName { name: Arc } => - swap_value(&mut track.name, name, |name|Self::SetName { name }), - SetColor { color: ItemTheme } => - swap_value(&mut track.color, color, |color|Self::SetColor { color }), - SetRec { rec: Option } => - toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }), - SetMon { mon: Option } => - toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }), -}); - /// Define a type alias for iterators of sized items (columns). macro_rules! def_sizes_iter { ($Type:ident => $($Item:ty),+) => { @@ -1177,196 +601,87 @@ pub(crate) const HEADER: &'static str = r#" /// Error, if any pub error: Arc>>> } - -/// Configuration: mode, view, and bind definitions. -/// -/// ``` -/// let config = tek::Config::default(); -/// ``` -/// -/// ``` -/// // Some dizzle. -/// // What indentation to use here lol? -/// let source = stringify!((mode :menu (name Menu) -/// (info Mode selector.) (keys :axis/y :confirm) -/// (view (bg (g 0) (bsp/s :ports/out -/// (bsp/n :ports/in -/// (bg (g 30) (bsp/s (fixed/y 7 :logo) -/// (fill :dialog/menu))))))))); -/// // Add this definition to the config and try to load it. -/// // A "mode" is basically a state machine -/// // with associated input and output definitions. -/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap(); -/// ``` -#[derive(Default, Debug)] pub struct Config { - /// XDG base directories of running user. - pub dirs: BaseDirectories, - /// Active collection of interaction modes. - pub modes: Modes, - /// Active collection of event bindings. - pub binds: Binds, - /// Active collection of view definitions. - pub views: Views, -} - -/// Group of view and keys definitions. -/// -/// ``` -/// let mode = tek::Mode::>::default(); -/// ``` -#[derive(Default, Debug)] pub struct Mode { - pub path: PathBuf, - pub name: Vec, - pub info: Vec, - pub view: Vec, - pub keys: Vec, - pub modes: Modes, -} - -/// An map of input events (e.g. [TuiEvent]) to [Binding]s. -/// -/// ``` -/// let lang = "(@x (nop)) (@y (nop) (nop))"; -/// let bind = tek::Bind::>::load(&lang).unwrap(); -/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1)); -/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2)); -/// ``` -#[derive(Debug)] pub struct Bind( - /// Map of each event (e.g. key combination) to - /// all command expressions bound to it by - /// all loaded input layers. - pub BTreeMap>> -); - -/// A sequence of zero or more commands (e.g. [AppCommand]), -/// optionally filtered by [Condition] to form layers. -/// -/// ``` -/// //FIXME: Why does it overflow? -/// //let binding: Binding<()> = tek::Binding { ..Default::default() }; -/// ``` -#[derive(Debug, Clone)] pub struct Binding { - pub commands: Arc<[C]>, - pub condition: Option, - pub description: Option>, - pub source: Option>, -} - -/// Condition that must evaluate to true in order to enable an input layer. -/// -/// ``` -/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true}))); -/// ``` -#[derive(Clone)] pub struct Condition( - pub Arcbool + Send + Sync>> -); - -/// List of menu items. -/// -/// ``` -/// let items: tek::MenuItems = Default::default(); -/// ``` -#[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems( - pub Arc<[MenuItem]> -); - -/// An item of a menu. -/// -/// ``` -/// let item: tek::MenuItem = Default::default(); -/// ``` -#[derive(Clone)] pub struct MenuItem( - /// Label - pub Arc, - /// Callback - pub ArcUsually<()> + Send + Sync>> -); - -/// The command-line interface descriptor. -/// -/// ``` -/// let cli: tek::Cli = Default::default(); -/// -/// use clap::CommandFactory; -/// tek::Cli::command().debug_assert(); -/// ``` -#[derive(Parser)] -#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] -#[derive(Debug, Default)] pub struct Cli { - /// Pre-defined configuration modes. - /// - /// TODO: Replace these with scripted configurations. - #[command(subcommand)] pub action: Action, -} - -/// Application modes that can be passed to the mommand line interface. -/// -/// ``` -/// let action: tek::Action = Default::default(); -/// ``` -#[derive(Debug, Clone, Subcommand, Default)] pub enum Action { - /// Continue where you left off - #[default] Resume, - /// Run headlessly in current session. - Headless, - /// Show status of current session. - Status, - /// List known sessions. - List, - /// Continue work in a copy of the current session. - Fork, - /// Create a new empty session. - New { - /// Name of JACK client - #[arg(short='n', long)] name: Option, - /// Whether to attempt to become transport master - #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, - /// Whether to sync to external transport master - #[arg(short='y', long, default_value_t = true)] sync_follow: bool, - /// Initial tempo in beats per minute - #[arg(short='b', long, default_value = None)] bpm: Option, - /// Whether to include a transport toolbar (default: true) - #[arg(short='c', long, default_value_t = true)] show_clock: bool, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='I', long)] midi_from: Vec, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='i', long)] midi_from_re: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='O', long)] midi_to: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='o', long)] midi_to_re: Vec, - /// Audio outs to connect to left input - #[arg(short='l', long)] left_from: Vec, - /// Audio outs to connect to right input - #[arg(short='r', long)] right_from: Vec, - /// Audio ins to connect from left output - #[arg(short='L', long)] left_to: Vec, - /// Audio ins to connect from right output - #[arg(short='R', long)] right_to: Vec, - /// Tracks to create - #[arg(short='t', long)] tracks: Option, - /// Scenes to create - #[arg(short='s', long)] scenes: Option, - }, - /// Import media as new session. - Import, - /// Show configuration. - Config, - /// Show version. - Version, -} - -pub type SceneWith<'a, T> = - (usize, &'a Scene, usize, usize, T); - -/// Collection of interaction modes. -pub type Modes = Arc, Arc>>>>>; - -/// Collection of input bindings. -pub type Binds = Arc, Bind>>>>; - -/// Collection of view definitions. -pub type Views = Arc, Arc>>>; +impl_has!(Clock: |self: App|self.project.clock); +impl_has!(Vec: |self: App|self.project.midi_ins); +impl_has!(Vec: |self: App|self.project.midi_outs); +impl_has!(Dialog: |self: App|self.dialog); +impl_has!(Jack<'static>: |self: App|self.jack); +impl_has!(Measure: |self: App|self.size); +impl_has!(Pool: |self: App|self.pool); +impl_has!(Selection: |self: App|self.project.selection); +impl_as_ref!(Vec: |self: App|self.project.as_ref()); +impl_as_mut!(Vec: |self: App|self.project.as_mut()); +impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); +impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); +impl_has_clips!( |self: App|self.pool.clips); +impl_audio!(App: tek_jack_process, tek_jack_event); +impl_handle!(TuiIn: |self: App, input|{ + let commands = collect_commands(self, input)?; + let history = execute_commands(self, commands)?; + self.history.extend(history.into_iter()); + Ok(None) +}); +namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); +namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); +namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| { + ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), + ":h/sample-detail" => 6.max(app.measure_height() as u16 * 3 / 9), }; }); +namespace!(App: isize { literal = |dsl|try_to_isize(dsl); }); +namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| { + ":scene-count" => app.scenes().len(), + ":track-count" => app.tracks().len(), + ":device-kind" => app.dialog.device_kind().unwrap_or(0), + ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), + ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), }; }); +namespace!(App: bool { symbol = |app| { // Provide boolean values. + ":mode/editor" => app.project.editor.is_some(), + ":focused/dialog" => !matches!(app.dialog, Dialog::None), + ":focused/message" => matches!(app.dialog, Dialog::Message(..)), + ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), + ":focused/browser" => app.dialog.browser().is_some(), + ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), + ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), + ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), + ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), + ":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}), + ":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)), + ":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)), + ":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix), +}; }); +namespace!(App: ItemTheme {}); // TODO: provide colors here +namespace!(App: Selection { symbol = |app| { + ":select/scene" => app.selection().select_scene(app.tracks().len()), + ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), + ":select/scene/prev" => app.selection().select_scene_prev(), + ":select/track" => app.selection().select_track(app.tracks().len()), + ":select/track/next" => app.selection().select_track_next(app.tracks().len()), + ":select/track/prev" => app.selection().select_track_prev(), +}; }); +namespace!(App: Color { + symbol = |app| { + ":color/bg" => Color::Rgb(28, 32, 36), + }; + expression = |app| { + "g" (n: u8) => Color::Rgb(n, n, n), + "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), + }; +}); +namespace!(App: Option { symbol = |app| { + ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) +}; }); +namespace!(App: Option { symbol = |app| { + ":selected/scene" => app.selection().scene(), + ":selected/track" => app.selection().track(), +}; }); +namespace!(App: Option>> { + symbol = |app| { + ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { + app.scenes()[*scene].clips[*track].clone() + } else { + None + } + }; +}); pub trait HasClipsSize { fn clips_size (&self) -> &Measure; } @@ -1382,415 +697,6 @@ pub trait HasWidth { fn width_dec (&mut self); } -/// A control axis. -/// -/// ``` -/// let axis = tek::ControlAxis::X; -/// ``` -#[derive(Debug, Copy, Clone)] pub enum ControlAxis { - X, Y, Z, I -} - -/// Various possible dialog modes. -/// -/// ``` -/// let dialog: tek::Dialog = Default::default(); -/// ``` -#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { - #[default] None, - Help(usize), - Menu(usize, MenuItems), - Device(usize), - Message(Arc), - Browse(BrowseTarget, Arc), - Options, -} - -/// Command-line configuration. -#[cfg(feature = "cli")] impl Cli { - pub fn run (&self) -> Usually<()> { - if let Action::Version = self.action { - return Ok(tek_show_version()) - } - - let mut config = Config::new(None); - config.init()?; - - if let Action::Config = self.action { - tek_print_config(&config); - } else if let Action::List = self.action { - todo!("list sessions") - } else if let Action::Resume = self.action { - todo!("resume session") - } else if let Action::New { - name, bpm, tracks, scenes, sync_lead, sync_follow, - midi_from, midi_from_re, midi_to, midi_to_re, - left_from, right_from, left_to, right_to, .. - } = &self.action { - - // Connect to JACK - let name = name.as_ref().map_or("tek", |x|x.as_str()); - let jack = Jack::new(&name)?; - - // TODO: Collect audio IO: - let empty = &[] as &[&str]; - let left_froms = Connect::collect(&left_from, empty, empty); - let left_tos = Connect::collect(&left_to, empty, empty); - let right_froms = Connect::collect(&right_from, empty, empty); - let right_tos = Connect::collect(&right_to, empty, empty); - let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; - let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; - - // Create initial project: - let clock = Clock::new(&jack, *bpm)?; - let mut project = Arrangement::new( - &jack, - None, - clock, - vec![], - vec![], - Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() - .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) - .collect::, _>>()?, - Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() - .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) - .collect::, _>>()? - ); - project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; - project.scenes_add(scenes.unwrap_or(0))?; - - if matches!(self.action, Action::Status) { - // Show status and exit - tek_print_status(&project); - return Ok(()) - } - - // Initialize the app state - let app = tek(&jack, project, config, ":menu"); - if matches!(self.action, Action::Headless) { - // TODO: Headless mode (daemon + client over IPC, then over network...) - println!("todo headless"); - return Ok(()) - } - - // Run the [Tui] and [Jack] threads with the [App] state. - Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ - - // Between jack init and app's first cycle: - - jack.sync_lead(*sync_lead, |mut state|{ - let clock = app.clock(); - clock.playhead.update_from_sample(state.position.frame() as f64); - state.position.bbt = Some(clock.bbt()); - state.position - })?; - - jack.sync_follow(*sync_follow)?; - - // FIXME: They don't work properly. - - Ok(app) - - })?)?; - } - Ok(()) - } -} - -impl Config { - const CONFIG_DIR: &'static str = "tek"; - const CONFIG_SUB: &'static str = "v0"; - const CONFIG: &'static str = "tek.edn"; - const DEFAULTS: &'static str = include_str!("./tek.edn"); - /// Create a new app configuration from a set of XDG base directories, - pub fn new (dirs: Option) -> Self { - let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); - let dirs = dirs.unwrap_or_else(default); - Self { dirs, ..Default::default() } - } - /// Write initial contents of configuration. - pub fn init (&mut self) -> Usually<()> { - self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ - cfgs.add(&dsl)?; - Ok(()) - })?; - Ok(()) - } - /// Write initial contents of a configuration file. - pub fn init_one ( - &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> - ) -> Usually<()> { - if self.dirs.find_config_file(path).is_none() { - //println!("Creating {path:?}"); - std::fs::write(self.dirs.place_config_file(path)?, defaults)?; - } - Ok(if let Some(path) = self.dirs.find_config_file(path) { - //println!("Loading {path:?}"); - let src = std::fs::read_to_string(&path)?; - src.as_str().each(move|item|each(self, item))?; - } else { - return Err(format!("{path}: not found").into()) - }) - } - /// Add statements to configuration from [Dsl] source. - pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { - dsl.each(|item|self.add_one(item))?; - Ok(self) - } - fn add_one (&self, item: impl Language) -> Usually<()> { - if let Some(expr) = item.expr()? { - let head = expr.head()?; - let tail = expr.tail()?; - let name = tail.head()?; - let body = tail.tail()?; - //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); - match head { - Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, - Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, - Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, - _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) - } - Ok(()) - } else { - return Err(format!("Config::load: expected expr, got: {item:?}").into()) - } - } - pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { - self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() - } -} - -impl Mode> { - /// Add a definition to the mode. - /// - /// Supported definitions: - /// - /// - (name ...) -> name - /// - (info ...) -> description - /// - (keys ...) -> key bindings - /// - (mode ...) -> submode - /// - ... -> view - /// - /// ``` - /// let mut mode: tek::Mode> = Default::default(); - /// mode.add("(name hello)").unwrap(); - /// ``` - pub fn add (&mut self, dsl: impl Language) -> Usually<()> { - Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { - //println!("Mode::add: {head} {:?}", expr.tail()); - let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - match head { - "name" => self.add_name(tail)?, - "info" => self.add_info(tail)?, - "keys" => self.add_keys(tail)?, - "mode" => self.add_mode(tail)?, - _ => self.add_view(tail)?, - }; - } else if let Ok(Some(word)) = dsl.word() { - self.add_view(word); - } else { - return Err(format!("Mode::add: unexpected: {dsl:?}").into()); - }) - - //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) - //.word(|word|self.add_view(word)) - //.expr(|expr|expr.head(|head|{ - ////println!("Mode::add: {head} {:?}", expr.tail()); - //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - //match head { - //"name" => self.add_name(tail), - //"info" => self.add_info(tail), - //"keys" => self.add_keys(tail)?, - //"mode" => self.add_mode(tail)?, - //_ => self.add_view(tail), - //}; - //})) - } - - fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.name.push(src.into()))) - } - fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.info.push(src.into()))) - } - fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.view.push(src.into()))) - } - fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) - } - fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(if let Some(id) = dsl.head()? { - load_mode(&self.modes, &id, &dsl.tail())?; - } else { - return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); - })) - } -} - -impl Bind { - /// Create a new event map - pub fn new () -> Self { - Default::default() - } - /// Add a binding to an owned event map. - pub fn def (mut self, event: E, binding: Binding) -> Self { - self.add(event, binding); - self - } - /// Add a binding to an event map. - pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { - if !self.0.contains_key(&event) { - self.0.insert(event.clone(), Default::default()); - } - self.0.get_mut(&event).unwrap().push(binding); - self - } - /// Return the binding(s) that correspond to an event. - pub fn query (&self, event: &E) -> Option<&[Binding]> { - self.0.get(event).map(|x|x.as_slice()) - } - /// Return the first binding that corresponds to an event, considering conditions. - pub fn dispatch (&self, event: &E) -> Option<&Binding> { - self.query(event) - .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) - .flatten() - } -} - -impl Bind> { - pub fn load (lang: &impl Language) -> Usually { - let mut map = Bind::new(); - lang.each(|item|if item.expr().head() == Ok(Some("see")) { - // TODO - Ok(()) - } else if let Ok(Some(_word)) = item.expr().head().word() { - if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { - map.add(key, Binding { - commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), - condition: None, - description: None, - source: None - }); - Ok(()) - } else if Some(":char") == item.expr()?.head()? { - // TODO - return Ok(()) - } else { - return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) - } - } else { - return Err(format!("Config::load_bind: unexpected: {item:?}").into()) - })?; - Ok(map) - } -} - -impl Dialog { - /// ``` - /// let _ = tek::Dialog::welcome(); - /// ``` - pub fn welcome () -> Self { - Self::Menu(1, MenuItems([ - MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), - MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ - app.dialog = Dialog::None; - app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); - })))), - MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), - ].into())) - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().menu_selected(); - /// ``` - pub fn menu_selected (&self) -> Option { - if let Self::Menu(selected, _) = self { Some(*selected) } else { None } - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().menu_next(); - /// ``` - pub fn menu_next (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), - _ => Self::None - } - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().menu_prev(); - /// ``` - pub fn menu_prev (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), - _ => Self::None - } - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().device_kind(); - /// ``` - pub fn device_kind (&self) -> Option { - if let Self::Device(index) = self { Some(*index) } else { None } - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().device_kind_next(); - /// ``` - pub fn device_kind_next (&self) -> Option { - self.device_kind().map(|index|(index + 1) % device_kinds().len()) - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().device_kind_prev(); - /// ``` - pub fn device_kind_prev (&self) -> Option { - self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) - } - /// FIXME: implement - pub fn message (&self) -> Option<&str> { todo!() } - /// FIXME: implement - pub fn browser (&self) -> Option<&Arc> { todo!() } - /// FIXME: implement - pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } -} -use crate::*; -impl_has!(Clock: |self: App|self.project.clock); -impl_has!(Vec: |self: App|self.project.midi_ins); -impl_has!(Vec: |self: App|self.project.midi_outs); -impl_has!(Dialog: |self: App|self.dialog); -impl_has!(Jack<'static>: |self: App|self.jack); -impl_has!(Measure: |self: App|self.size); -impl_has!(Pool: |self: App|self.pool); -impl_has!(Selection: |self: App|self.project.selection); -impl_as_ref!(Vec: |self: App|self.project.as_ref()); -impl_as_mut!(Vec: |self: App|self.project.as_mut()); -impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); -impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); -impl_has_clips!( |self: App|self.pool.clips); -impl_audio!(App: tek_jack_process, tek_jack_event); -impl_handle!(TuiIn: |self: App, input|{ - let commands = collect_commands(self, input)?; - let history = execute_commands(self, commands)?; - self.history.extend(history.into_iter()); - Ok(None) -}); - -impl Draw for App { - fn draw (self, to: &mut Tui) -> Usually> { - if let Some(e) = self.error.read().unwrap().as_ref() { - to.show(to.area(), e); - } - for (index, dsl) in self.mode.view.iter().enumerate() { - if let Err(e) = self.understand(to, dsl) { - *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); - break; - } - } - } -} - impl<'a> Namespace<'a, AppCommand> for App { symbols!('a |app| -> AppCommand { "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, @@ -1801,7 +707,6 @@ impl<'a> Namespace<'a, AppCommand> for App { "cancel" => AppCommand::Cancel, }); } - impl Understand for App { fn understand_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { app_understand_expr(self, to, lang) @@ -1810,7 +715,6 @@ impl Understand for App { app_understand_word(self, to, lang) } } - fn app_understand_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> { if evaluate_output_expression(state, to, lang)? || evaluate_output_expression_tui(state, to, lang)? { @@ -1819,7 +723,6 @@ fn app_understand_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Us Err(format!("App::understand_expr: unexpected: {lang:?}").into()) } } - fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> { let mut frags = dsl.src()?.unwrap().split("/"); match frags.next() { @@ -1924,7 +827,6 @@ fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usu } Ok(()) } - impl App { /// Update memoized render of clock values. /// ``` @@ -2045,137 +947,32 @@ impl App { } } } - -impl +AsMut> HasClock for T {} -impl +AsMut> HasSelection for T {} -impl +AsMut> HasSequencer for T {} -impl >+AsMut>> HasScenes for T {} -impl >+AsMut>> HasTracks for T {} -impl +AsMutOpt> HasEditor for T {} -impl +AsMutOpt+Send+Sync> HasScene for T {} -impl +AsMutOpt+Send+Sync> HasTrack for T {} -impl MidiPoint for T {} -impl > TracksView for T {} -impl MidiRange for T {} -impl ClipsView for T {} - -/// Default is always empty map regardless if `E` and `C` implement [Default]. -impl Default for Bind { - fn default () -> Self { Self(Default::default()) } -} -impl Default for Binding { - fn default () -> Self { - Self { - commands: Default::default(), - condition: Default::default(), - description: Default::default(), - source: Default::default(), +impl Draw for App { + fn draw (self, to: &mut Tui) -> Usually> { + if let Some(e) = self.error.read().unwrap().as_ref() { + to.show(to.area(), e); + } + for (index, dsl) in self.mode.view.iter().enumerate() { + if let Err(e) = self.understand(to, dsl) { + *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); + break; + } } } } - -impl_default!(AppCommand: Self::Nop); -impl_default!(MenuItem: Self("".into(), Arc::new(Box::new(|_|Ok(()))))); -impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ)); - +impl +AsMut> HasClock for T {} +impl +AsMut> HasSequencer for T {} +impl +AsMutOpt> HasEditor for T {} +impl MidiPoint for T {} +impl MidiRange for T {} impl Gettable for AtomicBool { fn get (&self) -> bool { self.load(Relaxed) } } impl InteriorMutable for AtomicBool { fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) } } impl Gettable for AtomicUsize { fn get (&self) -> usize { self.load(Relaxed) } } impl InteriorMutable for AtomicUsize { fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) } } - -impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } -impl AsRef> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } } impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.size_inner } } impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } - -#[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock); - -impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); -impl_debug!(Condition |self, w| { write!(w, "*") }); - +impl_default!(AppCommand: Self::Nop); primitive!(u8: try_to_u8); primitive!(u16: try_to_u16); primitive!(usize: try_to_usize); primitive!(isize: try_to_isize); -namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); -namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); -namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| { - ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), - ":h/sample-detail" => 6.max(app.measure_height() as u16 * 3 / 9), }; }); -namespace!(App: isize { literal = |dsl|try_to_isize(dsl); }); -namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| { - ":scene-count" => app.scenes().len(), - ":track-count" => app.tracks().len(), - ":device-kind" => app.dialog.device_kind().unwrap_or(0), - ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), - ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), }; }); -namespace!(App: bool { symbol = |app| { // Provide boolean values. - ":mode/editor" => app.project.editor.is_some(), - ":focused/dialog" => !matches!(app.dialog, Dialog::None), - ":focused/message" => matches!(app.dialog, Dialog::Message(..)), - ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), - ":focused/browser" => app.dialog.browser().is_some(), - ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), - ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), - ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), - ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), - ":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}), - ":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)), - ":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)), - ":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix), -}; }); -namespace!(App: ItemTheme {}); // TODO: provide colors here -namespace!(App: Dialog { symbol = |app| { - ":dialog/none" => Dialog::None, - ":dialog/options" => Dialog::Options, - ":dialog/device" => Dialog::Device(0), - ":dialog/device/prev" => Dialog::Device(0), - ":dialog/device/next" => Dialog::Device(0), - ":dialog/help" => Dialog::Help(0), - ":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, - Browse::new(None).unwrap().into()), - ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, - Browse::new(None).unwrap().into()), - ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), - Browse::new(None).unwrap().into()), -}; }); -namespace!(App: Selection { symbol = |app| { - ":select/scene" => app.selection().select_scene(app.tracks().len()), - ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), - ":select/scene/prev" => app.selection().select_scene_prev(), - ":select/track" => app.selection().select_track(app.tracks().len()), - ":select/track/next" => app.selection().select_track_next(app.tracks().len()), - ":select/track/prev" => app.selection().select_track_prev(), -}; }); -namespace!(App: Color { - symbol = |app| { - ":color/bg" => Color::Rgb(28, 32, 36), - }; - expression = |app| { - "g" (n: u8) => Color::Rgb(n, n, n), - "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), - }; -}); -namespace!(App: Option { symbol = |app| { - ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) -}; }); -namespace!(App: Option { symbol = |app| { - ":selected/scene" => app.selection().scene(), - ":selected/track" => app.selection().track(), -}; }); -namespace!(App: Option>> { - symbol = |app| { - ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { - app.scenes()[*scene].clips[*track].clone() - } else { - None - } - }; -}); diff --git a/src/view.rs b/src/view.rs new file mode 100644 index 00000000..018515fb --- /dev/null +++ b/src/view.rs @@ -0,0 +1,10 @@ +use crate::*; + +/// Collection of view definitions. +pub type Views = Arc, Arc>>>; + +pub(crate) fn load_view (views: &Views, name: &impl AsRef, body: &impl Language) -> Usually<()> { + views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into()); + Ok(()) +} + From 60dbd89fc91c7dcc0bc2a3e3a9a7f98290b03ae5 Mon Sep 17 00:00:00 2001 From: okay stopped screaming Date: Sat, 21 Mar 2026 23:53:24 +0200 Subject: [PATCH 6/7] wip: 284 errors, later --- src/arrange.rs | 210 ++++++++---------------------------------------- src/browse.rs | 5 +- src/cli.rs | 3 +- src/device.rs | 48 +++++++++++ src/dialog.rs | 2 +- src/sample.rs | 4 +- src/select.rs | 161 +++++++++++++++++++++++++++++++++++++ src/sequence.rs | 66 +-------------- src/tek.rs | 5 +- 9 files changed, 250 insertions(+), 254 deletions(-) create mode 100644 src/select.rs diff --git a/src/arrange.rs b/src/arrange.rs index a2aaf6d4..e336951f 100644 --- a/src/arrange.rs +++ b/src/arrange.rs @@ -1,8 +1,7 @@ -use crate::*; use ::std::sync::{Arc, RwLock}; use ::tengri::{space::east, color::ItemTheme}; use ::tengri::{draw::*, term::*}; -use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput}; +use crate::{*, device::*, sequence::*, clock::*, select::*, sample::*}; impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } /// Arranger. @@ -50,6 +49,40 @@ impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &sel #[cfg(feature = "scene")] pub scene_scroll: usize, } +/// A track consists of a sequencer and zero or more devices chained after it. +/// +/// ``` +/// let track: tek::Track = Default::default(); +/// ``` +#[derive(Debug, Default)] pub struct Track { + /// Name of track + pub name: Arc, + /// Identifying color of track + pub color: ItemTheme, + /// Preferred width of track column + pub width: usize, + /// MIDI sequencer state + pub sequencer: Sequencer, + /// Device chain + pub devices: Vec, +} + +/// A scene consists of a set of clips to play together. +/// +/// ``` +/// let scene: tek::Scene = Default::default(); +/// let _ = scene.pulses(); +/// let _ = scene.is_playing(&[]); +/// ``` +#[derive(Debug, Default)] pub struct Scene { + /// Name of scene + pub name: Arc, + /// Identifying color of scene + pub color: ItemTheme, + /// Clips in scene, one per track + pub clips: Vec>>>, +} + impl_has!(Jack<'static>: |self: Arrangement| self.jack); impl_has!(Measure: |self: Arrangement| self.size); impl_has!(Vec: |self: Arrangement| self.tracks); @@ -63,7 +96,6 @@ impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); -impl +AsMut> HasSelection for T {} impl >+AsMut>> HasScenes for T {} impl >+AsMut>> HasTracks for T {} impl +AsMutOpt+Send+Sync> HasScene for T {} @@ -141,47 +173,6 @@ pub trait ClipsView: TracksView + ScenesView { } -/// Represents the current user selection in the arranger -#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { - #[default] - /// Nothing is selected - Nothing, - /// The whole mix is selected - Mix, - /// A MIDI input is selected. - Input(usize), - /// A MIDI output is selected. - Output(usize), - /// A scene is selected. - #[cfg(feature = "scene")] Scene(usize), - /// A track is selected. - #[cfg(feature = "track")] Track(usize), - /// A clip (track × scene) is selected. - #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, - /// A track's MIDI input connection is selected. - #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, - /// A track's MIDI output connection is selected. - #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, - /// A track device slot is selected. - #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, -} - -/// A scene consists of a set of clips to play together. -/// -/// ``` -/// let scene: tek::Scene = Default::default(); -/// let _ = scene.pulses(); -/// let _ = scene.is_playing(&[]); -/// ``` -#[derive(Debug, Default)] pub struct Scene { - /// Name of scene - pub name: Arc, - /// Identifying color of scene - pub color: ItemTheme, - /// Clips in scene, one per track - pub clips: Vec>>>, -} - pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured { fn tracks_width_available (&self) -> u16 { @@ -330,39 +321,6 @@ pub trait HasScene: AsRefOpt + AsMutOpt { fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() } fn scene (&self) -> Option<&Scene> { self.as_ref_opt() } } -pub trait HasSelection: AsRef + AsMut { - fn selection (&self) -> &Selection { self.as_ref() } - fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } - /// Get the active track - #[cfg(feature = "track")] - fn selected_track (&self) -> Option<&Track> where Self: HasTracks { - let index = self.selection().track()?; - self.tracks().get(index) - } - /// Get a mutable reference to the active track - #[cfg(feature = "track")] - fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { - let index = self.selection().track()?; - self.tracks_mut().get_mut(index) - } - /// Get the active scene - #[cfg(feature = "scene")] - fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { - let index = self.selection().scene()?; - self.scenes().get(index) - } - /// Get a mutable reference to the active scene - #[cfg(feature = "scene")] - fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { - let index = self.selection().scene()?; - self.scenes_mut().get_mut(index) - } - /// Get the active clip - #[cfg(feature = "clip")] - fn selected_clip (&self) -> Option>> where Self: HasScenes + HasTracks { - self.selected_scene()?.clips.get(self.selection().track()?)?.clone() - } -} pub trait HasScenes: AsRef> + AsMut> { fn scenes (&self) -> &Vec { self.as_ref() } fn scenes_mut (&mut self) -> &mut Vec { self.as_mut() } @@ -599,104 +557,6 @@ pub trait HasTrack: AsRefOpt + AsMutOpt { fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } } -impl Selection { - pub fn describe ( - &self, - #[cfg(feature = "track")] tracks: &[Track], - #[cfg(feature = "scene")] scenes: &[Scene], - ) -> Arc { - use Selection::*; - format!("{}", match self { - Mix => "Everything".to_string(), - #[cfg(feature = "scene")] Scene(s) => - scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), - #[cfg(feature = "track")] Track(t) => - tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), - TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { - (Some(_), Some(s)) => match s.clip(*track) { - Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), - None => format!("T{track} S{scene}: Empty") - }, - _ => format!("T{track} S{scene}: Empty"), - }, - _ => todo!() - }).into() - } - #[cfg(feature = "scene")] pub fn scene (&self) -> Option { - use Selection::*; - match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } - } - #[cfg(feature = "scene")] pub fn select_scene (&self, scene_count: usize) -> Self { - use Selection::*; - match self { - Mix | Track(_) => Scene(0), - Scene(s) => Scene((s + 1) % scene_count), - TrackClip { scene, .. } => Track(*scene), - _ => todo!(), - } - } - #[cfg(feature = "scene")] pub fn select_scene_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Scene(0), - Track(t) => TrackClip { track: *t, scene: 0 }, - Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, - TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, - _ => todo!() - } - } - #[cfg(feature = "scene")] pub fn select_scene_prev (&self) -> Self { - use Selection::*; - match self { - Mix | Scene(0) => Mix, - Scene(s) => Scene(s - 1), - Track(t) => Track(*t), - TrackClip { track, scene: 0 } => Track(*track), - TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, - _ => todo!() - } - } - #[cfg(feature = "track")] pub fn track (&self) -> Option { - use Selection::*; - if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { - Some(*track) - } else { - None - } - } - #[cfg(feature = "track")] pub fn select_track (&self, track_count: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(_) => Mix, - Track(t) => Track((t + 1) % track_count), - TrackClip { track, .. } => Track(*track), - _ => todo!(), - } - } - #[cfg(feature = "track")] pub fn select_track_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(s) => TrackClip { track: 0, scene: *s }, - Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, - TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, - _ => todo!() - } - } - #[cfg(feature = "track")] pub fn select_track_prev (&self) -> Self { - use Selection::*; - match self { - Mix => Mix, - Scene(s) => Scene(*s), - Track(0) => Mix, - Track(t) => Track(t - 1), - TrackClip { track: 0, scene } => Scene(*scene), - TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, - _ => todo!() - } - } -} impl Arrangement { /// Create a new arrangement. pub fn new ( diff --git a/src/browse.rs b/src/browse.rs index 48dda692..d0289fe5 100644 --- a/src/browse.rs +++ b/src/browse.rs @@ -1,7 +1,4 @@ -use crate::*; -use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; -use crate::sequence::MidiClip; -use crate::sample::Sample; +use crate::{*, clock::*, sequence::*, sample::*}; def_command!(FileBrowserCommand: |sampler: Sampler|{ //("begin" [] Some(Self::Begin)) diff --git a/src/cli.rs b/src/cli.rs index 6c8bc6cf..4373c13f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,4 @@ -use crate::*; -use crate::*; +use crate::{*, arrange::*, clock::*, config::*, device::*}; /// The command-line interface descriptor. /// diff --git a/src/device.rs b/src/device.rs index f914f8bc..c87f7c2a 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,4 +1,7 @@ use crate::*; +use ConnectName::*; +use ConnectScope::*; +use ConnectStatus::*; def_command!(DeviceCommand: |device: Device| {}); @@ -620,3 +623,48 @@ impl> + AsMut>> HasDevices for T { self.as_mut() } } +/// Trait for thing that may receive MIDI. +pub trait HasMidiIns { + fn midi_ins (&self) -> &Vec; + fn midi_ins_mut (&mut self) -> &mut Vec; + /// Collect MIDI input from app ports (TODO preallocate large buffers) + fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { + self.midi_ins().iter() + .map(|port|port.port().iter(scope) + .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) + .collect::>()) + .collect::>() + } + fn midi_ins_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_ins().iter().enumerate().map(move|(i, input)|{ + let height = 1 + input.connections().len(); + let data = (i, input.port_name(), input.connections(), y, y + height); + y += height; + data + }) + } +} +/// Trait for thing that may output MIDI. +pub trait HasMidiOuts { + fn midi_outs (&self) -> &Vec; + fn midi_outs_mut (&mut self) -> &mut Vec; + fn midi_outs_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_outs().iter().enumerate().map(move|(i, output)|{ + let height = 1 + output.connections().len(); + let data = (i, output.port_name(), output.connections(), y, y + height); + y += height; + data + }) + } + fn midi_outs_emit (&mut self, scope: &ProcessScope) { + for port in self.midi_outs_mut().iter_mut() { + port.buffer_emit(scope) + } + } +} diff --git a/src/dialog.rs b/src/dialog.rs index 61baec11..492ebed8 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{*, menu::*}; /// Various possible dialog modes. /// diff --git a/src/sample.rs b/src/sample.rs index 4dc65344..6196b93f 100644 --- a/src/sample.rs +++ b/src/sample.rs @@ -1,6 +1,4 @@ -use crate::*; -use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; -use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput}; +use crate::{*, device::*, browse::*, mix::*}; def_command!(SamplerCommand: |sampler: Sampler| { RecordToggle { slot: usize } => { diff --git a/src/select.rs b/src/select.rs new file mode 100644 index 00000000..ae748e5d --- /dev/null +++ b/src/select.rs @@ -0,0 +1,161 @@ +use crate::*; + +/// Represents the current user selection in the arranger +#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { + #[default] + /// Nothing is selected + Nothing, + /// The whole mix is selected + Mix, + /// A MIDI input is selected. + Input(usize), + /// A MIDI output is selected. + Output(usize), + /// A scene is selected. + #[cfg(feature = "scene")] Scene(usize), + /// A track is selected. + #[cfg(feature = "track")] Track(usize), + /// A clip (track × scene) is selected. + #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, + /// A track's MIDI input connection is selected. + #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, + /// A track's MIDI output connection is selected. + #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, + /// A track device slot is selected. + #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, +} + +impl Selection { + pub fn describe ( + &self, + #[cfg(feature = "track")] tracks: &[Track], + #[cfg(feature = "scene")] scenes: &[Scene], + ) -> Arc { + use Selection::*; + format!("{}", match self { + Mix => "Everything".to_string(), + #[cfg(feature = "scene")] Scene(s) => + scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), + #[cfg(feature = "track")] Track(t) => + tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), + TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { + (Some(_), Some(s)) => match s.clip(*track) { + Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), + None => format!("T{track} S{scene}: Empty") + }, + _ => format!("T{track} S{scene}: Empty"), + }, + _ => todo!() + }).into() + } + #[cfg(feature = "scene")] pub fn scene (&self) -> Option { + use Selection::*; + match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } + } + #[cfg(feature = "scene")] pub fn select_scene (&self, scene_count: usize) -> Self { + use Selection::*; + match self { + Mix | Track(_) => Scene(0), + Scene(s) => Scene((s + 1) % scene_count), + TrackClip { scene, .. } => Track(*scene), + _ => todo!(), + } + } + #[cfg(feature = "scene")] pub fn select_scene_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Scene(0), + Track(t) => TrackClip { track: *t, scene: 0 }, + Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, + TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, + _ => todo!() + } + } + #[cfg(feature = "scene")] pub fn select_scene_prev (&self) -> Self { + use Selection::*; + match self { + Mix | Scene(0) => Mix, + Scene(s) => Scene(s - 1), + Track(t) => Track(*t), + TrackClip { track, scene: 0 } => Track(*track), + TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, + _ => todo!() + } + } + #[cfg(feature = "track")] pub fn track (&self) -> Option { + use Selection::*; + if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { + Some(*track) + } else { + None + } + } + #[cfg(feature = "track")] pub fn select_track (&self, track_count: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(_) => Mix, + Track(t) => Track((t + 1) % track_count), + TrackClip { track, .. } => Track(*track), + _ => todo!(), + } + } + #[cfg(feature = "track")] pub fn select_track_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(s) => TrackClip { track: 0, scene: *s }, + Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, + TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, + _ => todo!() + } + } + #[cfg(feature = "track")] pub fn select_track_prev (&self) -> Self { + use Selection::*; + match self { + Mix => Mix, + Scene(s) => Scene(*s), + Track(0) => Mix, + Track(t) => Track(t - 1), + TrackClip { track: 0, scene } => Scene(*scene), + TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, + _ => todo!() + } + } +} + +impl +AsMut> HasSelection for T {} + +pub trait HasSelection: AsRef + AsMut { + fn selection (&self) -> &Selection { self.as_ref() } + fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } + /// Get the active track + #[cfg(feature = "track")] + fn selected_track (&self) -> Option<&Track> where Self: HasTracks { + let index = self.selection().track()?; + self.tracks().get(index) + } + /// Get a mutable reference to the active track + #[cfg(feature = "track")] + fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { + let index = self.selection().track()?; + self.tracks_mut().get_mut(index) + } + /// Get the active scene + #[cfg(feature = "scene")] + fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { + let index = self.selection().scene()?; + self.scenes().get(index) + } + /// Get a mutable reference to the active scene + #[cfg(feature = "scene")] + fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { + let index = self.selection().scene()?; + self.scenes_mut().get_mut(index) + } + /// Get the active clip + #[cfg(feature = "clip")] + fn selected_clip (&self) -> Option>> where Self: HasScenes + HasTracks { + self.selected_scene()?.clips.get(self.selection().track()?)?.clone() + } +} diff --git a/src/sequence.rs b/src/sequence.rs index e697290b..630318d5 100644 --- a/src/sequence.rs +++ b/src/sequence.rs @@ -1,5 +1,4 @@ -use crate::*; -use ::std::sync::{Arc, RwLock}; +use crate::{*, device::*}; def_command!(MidiEditCommand: |editor: MidiEditor| { Show { clip: Option>> } => { @@ -155,24 +154,6 @@ pub struct Sequencer { pub midi_buf: Vec>>, } -/// A track consists of a sequencer and zero or more devices chained after it. -/// -/// ``` -/// let track: tek::Track = Default::default(); -/// ``` -#[derive(Debug, Default)] pub struct Track { - /// Name of track - pub name: Arc, - /// Identifying color of track - pub color: ItemTheme, - /// Preferred width of track column - pub width: usize, - /// MIDI sequencer state - pub sequencer: Sequencer, - /// Device chain - pub devices: Vec, -} - pub trait HasPlayClip: HasClock { @@ -423,51 +404,6 @@ pub trait HasEditor: AsRefOpt + AsMutOpt { fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) as usize } fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) as usize } } -/// Trait for thing that may receive MIDI. -pub trait HasMidiIns { - fn midi_ins (&self) -> &Vec; - fn midi_ins_mut (&mut self) -> &mut Vec; - /// Collect MIDI input from app ports (TODO preallocate large buffers) - fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { - self.midi_ins().iter() - .map(|port|port.port().iter(scope) - .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) - .collect::>()) - .collect::>() - } - fn midi_ins_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_ins().iter().enumerate().map(move|(i, input)|{ - let height = 1 + input.connections().len(); - let data = (i, input.port_name(), input.connections(), y, y + height); - y += height; - data - }) - } -} -/// Trait for thing that may output MIDI. -pub trait HasMidiOuts { - fn midi_outs (&self) -> &Vec; - fn midi_outs_mut (&mut self) -> &mut Vec; - fn midi_outs_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_outs().iter().enumerate().map(move|(i, output)|{ - let height = 1 + output.connections().len(); - let data = (i, output.port_name(), output.connections(), y, y + height); - y += height; - data - }) - } - fn midi_outs_emit (&mut self, scope: &ProcessScope) { - for port in self.midi_outs_mut().iter_mut() { - port.buffer_emit(scope) - } - } -} pub trait HasMidiClip { fn clip (&self) -> Option>>; } diff --git a/src/tek.rs b/src/tek.rs index 838c8db4..3210d078 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -52,6 +52,7 @@ pub mod mode; pub mod plugin; pub mod sample; pub mod sequence; +pub mod select; pub mod view; use clap::{self, Parser, Subcommand}; @@ -965,10 +966,6 @@ impl +AsMut> HasSequencer for T {} impl +AsMutOpt> HasEditor for T {} impl MidiPoint for T {} impl MidiRange for T {} -impl Gettable for AtomicBool { fn get (&self) -> bool { self.load(Relaxed) } } -impl InteriorMutable for AtomicBool { fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) } } -impl Gettable for AtomicUsize { fn get (&self) -> usize { self.load(Relaxed) } } -impl InteriorMutable for AtomicUsize { fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) } } impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.size_inner } } impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } impl_default!(AppCommand: Self::Nop); From 9ef63324af597e72de1fd59675e72af14b423e0d Mon Sep 17 00:00:00 2001 From: okay stopped screaming Date: Sun, 22 Mar 2026 00:00:21 +0200 Subject: [PATCH 7/7] wip: 1 more pass, 82e, near there gotta replace that Measure thing with RwLock<[u16;2]> --- src/clock.rs | 1 + src/config.rs | 2 +- src/dialog.rs | 2 +- src/mode.rs | 3 ++- src/sequence.rs | 28 ++++++++++++++++------------ src/tek.rs | 11 +++++------ 6 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/clock.rs b/src/clock.rs index 16c8de23..2e76cbc6 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -3,6 +3,7 @@ use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; use ::atomic_float::AtomicF64; use ::tengri::{draw::*, term::*}; +impl +AsMut> HasClock for T {} pub trait HasClock: AsRef + AsMut { fn clock (&self) -> &Clock { self.as_ref() } fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } diff --git a/src/config.rs b/src/config.rs index d550e789..415f99b8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{*, bind::*, mode::*, view::*}; /// Configuration: mode, view, and bind definitions. /// diff --git a/src/dialog.rs b/src/dialog.rs index 492ebed8..56ae8043 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -1,4 +1,4 @@ -use crate::{*, menu::*}; +use crate::{*, browse::*, device::*, menu::*}; /// Various possible dialog modes. /// diff --git a/src/mode.rs b/src/mode.rs index 46692c39..8577d80b 100644 --- a/src/mode.rs +++ b/src/mode.rs @@ -1,4 +1,5 @@ -use crate::*; +use crate::{*, config::*}; + impl Config { pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() diff --git a/src/sequence.rs b/src/sequence.rs index 630318d5..7c3fadae 100644 --- a/src/sequence.rs +++ b/src/sequence.rs @@ -1,5 +1,9 @@ -use crate::{*, device::*}; +use crate::{*, clock::*, device::*}; +impl +AsMut> HasSequencer for T {} +impl +AsMutOpt> HasEditor for T {} +impl MidiPoint for T {} +impl MidiRange for T {} def_command!(MidiEditCommand: |editor: MidiEditor| { Show { clip: Option>> } => { editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) }, @@ -461,32 +465,32 @@ pub trait MidiPoint: NotePoint + TimePoint { pub trait TimeRange { fn time_len (&self) -> &AtomicUsize; fn get_time_len (&self) -> usize { - self.time_len().load(Ordering::Relaxed) + self.time_len().load(Relaxed) } fn time_zoom (&self) -> &AtomicUsize; fn get_time_zoom (&self) -> usize { - self.time_zoom().load(Ordering::Relaxed) + self.time_zoom().load(Relaxed) } fn set_time_zoom (&self, value: usize) -> usize { - self.time_zoom().swap(value, Ordering::Relaxed) + self.time_zoom().swap(value, Relaxed) } fn time_lock (&self) -> &AtomicBool; fn get_time_lock (&self) -> bool { - self.time_lock().load(Ordering::Relaxed) + self.time_lock().load(Relaxed) } fn set_time_lock (&self, value: bool) -> bool { - self.time_lock().swap(value, Ordering::Relaxed) + self.time_lock().swap(value, Relaxed) } fn time_start (&self) -> &AtomicUsize; fn get_time_start (&self) -> usize { - self.time_start().load(Ordering::Relaxed) + self.time_start().load(Relaxed) } fn set_time_start (&self, value: usize) -> usize { - self.time_start().swap(value, Ordering::Relaxed) + self.time_start().swap(value, Relaxed) } fn time_axis (&self) -> &AtomicUsize; fn get_time_axis (&self) -> usize { - self.time_axis().load(Ordering::Relaxed) + self.time_axis().load(Relaxed) } fn get_time_end (&self) -> usize { self.time_start().get() + self.time_axis().get() * self.time_zoom().get() @@ -496,14 +500,14 @@ pub trait TimeRange { pub trait NoteRange { fn note_lo (&self) -> &AtomicUsize; fn get_note_lo (&self) -> usize { - self.note_lo().load(Ordering::Relaxed) + self.note_lo().load(Relaxed) } fn set_note_lo (&self, x: usize) -> usize { - self.note_lo().swap(x, Ordering::Relaxed) + self.note_lo().swap(x, Relaxed) } fn note_axis (&self) -> &AtomicUsize; fn get_note_axis (&self) -> usize { - self.note_axis().load(Ordering::Relaxed) + self.note_axis().load(Relaxed) } fn get_note_hi (&self) -> usize { (self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127) diff --git a/src/tek.rs b/src/tek.rs index 3210d078..31e5166e 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -57,6 +57,10 @@ pub mod view; use clap::{self, Parser, Subcommand}; use builder_pattern::Builder; +use self::{ + arrange::*, clock::*, dialog::*, browse::*, select::*, sequence::*, device::*, + config::*, mode::*, view::*, bind::* +}; extern crate xdg; pub(crate) use ::xdg::BaseDirectories; @@ -961,14 +965,9 @@ impl Draw for App { } } } -impl +AsMut> HasClock for T {} -impl +AsMut> HasSequencer for T {} -impl +AsMutOpt> HasEditor for T {} -impl MidiPoint for T {} -impl MidiRange for T {} impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.size_inner } } impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl_default!(AppCommand: Self::Nop); +impl_default!(AppCommand: Self::Nop); primitive!(u8: try_to_u8); primitive!(u16: try_to_u16); primitive!(usize: try_to_usize);