From 9ae35830c376a602675eb46ab0ceb4750fa58c85 Mon Sep 17 00:00:00 2001 From: okay stopped screaming Date: Mon, 23 Feb 2026 18:40:30 +0200 Subject: [PATCH 1/4] wip: removing Has trait --- Justfile | 6 +- app/tek.rs | 87 +---- app/tek_impls.rs | 866 ++++++++++++++++++++++++++--------------------- app/tek_trait.rs | 99 ++---- dizzle | 2 +- 5 files changed, 520 insertions(+), 540 deletions(-) diff --git a/Justfile b/Justfile index 0d60bdc7..ebb68b1c 100644 --- a/Justfile +++ b/Justfile @@ -4,6 +4,9 @@ export RUST_BACKTRACE := "1" default +ARGS="new": target/debug/tek {{ARGS}} +doc +ARGS="": + cargo doc --open -j4 --document-private-items {{ARGS}} + cloc: for src in {cli,edn/src,input/src,jack/src,midi/src,output/src,plugin/src,sampler/src,tek/src,time/src,tui/src}; do echo; echo $src; cloc --quiet $src; done @@ -43,9 +46,6 @@ run-init: prof: CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -- -doc: - cargo doc -j4 --workspace --document-private-items - release := "reset && cargo run --release --" release: {{release}} diff --git a/app/tek.rs b/app/tek.rs index 1b2b1a73..8cd455fc 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -384,12 +384,12 @@ pub fn device_kinds () -> &'static [&'static str] { ] } -impl>> HasDevices for T { +impl> + AsMut>> HasDevices for T { fn devices (&self) -> &Vec { - self.get() + self.as_ref() } fn devices_mut (&mut self) -> &mut Vec { - self.get_mut() + self.as_mut() } } @@ -1127,26 +1127,6 @@ mod view { Fixed::XY(20, 1 + ins, frame.enclose(Fixed::XY(20, 1 + ins, field))) } - pub fn per_track_top <'a, T: Content + '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, - 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( - track.color.lightest.rgb, - track.color.base.rgb, - callback(index, track))))}))) - } - - pub fn per_track <'a, T: Content + '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)))) - } - pub fn io_ports <'a, T: PortsSizes<'a>> ( fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a ) -> impl Content + 'a { @@ -1159,67 +1139,6 @@ mod view { Fill::Y(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, &connect.info))))))))) } - - #[cfg(feature = "lv2")] pub fn draw_header (state: &Lv2, to: &mut TuiOut, 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 }) - } - - pub fn draw_sample ( - to: &mut TuiOut, 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) - } -} - -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. -pub(crate) 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 - } - _ => {} - } - } } #[cfg(test)] mod test_view_meter { diff --git a/app/tek_impls.rs b/app/tek_impls.rs index 83fc76fe..12987408 100644 --- a/app/tek_impls.rs +++ b/app/tek_impls.rs @@ -2,350 +2,358 @@ use crate::*; use std::fmt::Write; use std::path::PathBuf; -impl Draw for App { - fn draw (&self, to: &mut TuiOut) { - if let Some(e) = self.error.read().unwrap().as_ref() { - to.place_at(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; +mod app { + use crate::*; + + impl Draw for App { + fn draw (&self, to: &mut TuiOut) { + if let Some(e) = self.error.read().unwrap().as_ref() { + to.place_at(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; + } } } } -} -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<'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, + 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 Understand for App { - fn understand_expr <'a> (&'a self, to: &mut TuiOut, lang: &'a impl Expression) -> Usually<()> { - app_understand_expr(self, to, lang) + 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, + }); } - fn understand_word <'a> (&'a self, to: &mut TuiOut, lang: &'a impl Expression) -> Usually<()> { - app_understand_word(self, to, lang) - } -} -fn app_understand_expr (state: &App, to: &mut TuiOut, 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()) + impl Understand for App { + fn understand_expr <'a> (&'a self, to: &mut TuiOut, 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<()> { + app_understand_word(self, to, lang) + } } -} -fn app_understand_word (state: &App, to: &mut TuiOut, 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(":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")))), - _ => 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")))), - _ => 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(Fill::XY(Thunk::new(move|to: &mut TuiOut|{ - for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + fn app_understand_expr (state: &App, to: &mut TuiOut, 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 TuiOut, 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(":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")))), + _ => 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")))), + _ => 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(Fill::XY(Thunk::new(move|to: &mut TuiOut|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + to.place(&Push::Y((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))) + ))); + } + }))) + } 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; + Fixed::Y(height, Min::X(30, Thunk::new(move |to: &mut TuiOut|{ + 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, - 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))) - ))); + Fixed::Y(2, Fill::X(Tui::bg(bg, Bsp::s( + Bsp::a(field_name, field_id), field_info)))))); } }))) - } 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; - Fixed::Y(height, Min::X(30, Thunk::new(move |to: &mut TuiOut|{ - 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)); + Some(":sessions") => to.place(&Fixed::Y(6, Min::X(30, Thunk::new(|to: &mut TuiOut|{ + 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, Bsp::s( - Bsp::a(field_name, field_id), field_info)))))); + &Fixed::Y(2, Fill::X(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); } - }))) - }), - Some(":sessions") => to.place(&Fixed::Y(6, Min::X(30, Thunk::new(|to: &mut TuiOut|{ - 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))))))); - } - })))), - Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(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:", - }, Shrink::X(3, Fixed::Y(1, Tui::fg(Tui::g(96), Repeat::X("๐Ÿญป")))))))), - Some(":device") => { - let selected = state.dialog.device_kind().unwrap(); - to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::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")))) }))) - }, - Some(":debug") => to.place(&Fixed::Y(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 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())) + Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(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:", + }, Shrink::X(3, Fixed::Y(1, Tui::fg(Tui::g(96), Repeat::X("๐Ÿญป")))))))), + Some(":device") => { + let selected = state.dialog.device_kind().unwrap(); + to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::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")))) }))) + }, + Some(":debug") => to.place(&Fixed::Y(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(()) } - /// 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 + + 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()); + } + } } } - /// 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 +} + +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!() } } - /// 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!() } } #[cfg(feature = "vst2")] impl ::vst::host::Host for Plugin {} @@ -353,26 +361,14 @@ impl Dialog { mod arrange { use crate::*; - has!(Jack<'static>: |self: Arrangement|self.jack); - has!(Measure: |self: Arrangement|self.size); + impl_has!(Jack<'static>: |self: Arrangement|self.jack); + impl_has!(Measure: |self: Arrangement|self.size); - #[cfg(feature = "editor")] has!(Option: |self: Arrangement|self.editor); - #[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_ins); - #[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_outs); - #[cfg(feature = "clock")] has!(Clock: |self: Arrangement|self.clock); - #[cfg(feature = "select")] has!(Selection: |self: Arrangement|self.selection); - - #[cfg(feature = "track")] impl TracksView for Arrangement {} // -> to auto? - - #[cfg(all(feature = "select", feature = "track"))] has!(Vec: |self: Arrangement|self.tracks); - #[cfg(all(feature = "select", feature = "track"))] maybe_has!(Track: |self: Arrangement| - { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; - { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); - - #[cfg(all(feature = "select", feature = "scene"))] has!(Vec: |self: Arrangement|self.scenes); - #[cfg(all(feature = "select", feature = "scene"))] maybe_has!(Scene: |self: Arrangement| - { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; - { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); + #[cfg(feature = "editor")] impl_has!(Option: |self: Arrangement|self.editor); + #[cfg(feature = "port")] impl_has!(Vec: |self: Arrangement|self.midi_ins); + #[cfg(feature = "port")] impl_has!(Vec: |self: Arrangement|self.midi_outs); + #[cfg(feature = "clock")] impl_has!(Clock: |self: Arrangement|self.clock); + #[cfg(feature = "select")] impl_has!(Selection: |self: Arrangement|self.selection); #[cfg(feature = "select")] impl Arrangement { #[cfg(feature = "clip")] fn selected_clip (&self) -> Option { todo!() } @@ -748,10 +744,7 @@ mod browse { //take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -impl> HasClock for T { - fn clock (&self) -> &Clock { self.get() } - fn clock_mut (&mut self) -> &mut Clock { self.get_mut() } -} +impl + AsMut> HasClock for T {} impl std::fmt::Debug for Clock { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { @@ -1030,9 +1023,9 @@ impl ClockView { } -impl>> HasEditor for T {} +impl>> HasEditor for T {} -has!(Measure: |self: MidiEditor|self.size); +impl_has!(Measure: |self: MidiEditor|self.size); impl std::fmt::Debug for MidiEditor { @@ -1200,7 +1193,7 @@ impl HasContent for MidiEditor { fn content (&self) -> impl Content { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } } -has!(Measure:|self:PianoHorizontal|self.size); +impl_has!(Measure:|self:PianoHorizontal|self.size); impl PianoHorizontal { pub fn new (clip: Option<&Arc>>) -> Self { @@ -1634,7 +1627,7 @@ impl Selection { } -impl> HasSelection for T {} +impl + AsMut> HasSelection for T {} /// Default is always empty map regardless if `E` and `C` implement [Default]. impl Default for Bind { @@ -1733,43 +1726,24 @@ impl> RegisterPorts for J { } } -#[cfg(feature = "scene")] impl AddScene for T {} -#[cfg(feature = "scene")] impl> + Send + Sync> HasScene for T {} -#[cfg(feature = "scene")] impl> + Send + Sync> HasScenes for T {} -#[cfg(feature = "scene")] impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } -#[cfg(feature = "scene")] impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } -#[cfg(feature = "scene")] has!(Vec: |self: App|self.project.scenes); -#[cfg(feature = "scene")] maybe_has!(Scene: |self: App| - { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); +#[cfg(feature = "clock")] impl_has!(Clock: |self: App|self.project.clock); +#[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock); +#[cfg(feature = "clock")] impl_has!(Clock: |self: Sequencer|self.clock); -#[cfg(feature = "track")] impl> + Send + Sync> HasTracks for T {} -#[cfg(feature = "track")] impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } -#[cfg(feature = "track")] impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } } -#[cfg(feature = "track")] has!(Vec: |self: App|self.project.tracks); -#[cfg(feature = "track")] maybe_has!(Track: |self: App| - { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); - -#[cfg(feature = "clock")] has!(Clock: |self: App|self.project.clock); -#[cfg(feature = "clock")] has!(Clock: |self: Track|self.sequencer.clock); -#[cfg(feature = "clock")] has!(Clock: |self: Sequencer|self.clock); - -#[cfg(feature = "port")] has!(Vec: |self: App|self.project.midi_ins); -#[cfg(feature = "port")] has!(Vec: |self: App|self.project.midi_outs); -#[cfg(feature = "port")] has!(Vec: |self: Sequencer|self.midi_ins); -#[cfg(feature = "port")] has!(Vec: |self: Sequencer|self.midi_outs); +#[cfg(feature = "port")] impl_has!(Vec: |self: App|self.project.midi_ins); +#[cfg(feature = "port")] impl_has!(Vec: |self: App|self.project.midi_outs); +#[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_ins); +#[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_outs); audio!(App: tek_jack_process, tek_jack_event); -audio!(Sampler: sampler_jack_process); -has!(Jack<'static>: |self: App|self.jack); -has!(Dialog: |self: App|self.dialog); -has!(Measure: |self: App|self.size); -has!(Option: |self: App|self.project.editor); -has!(Pool: |self: App|self.pool); -has!(Selection: |self: App|self.project.selection); -has!(Sequencer: |self: Track|self.sequencer); +impl_has!(Jack<'static>: |self: App|self.jack); +impl_has!(Dialog: |self: App|self.dialog); +impl_has!(Measure: |self: App|self.size); +impl_has!(Option: |self: App|self.project.editor); +impl_has!(Pool: |self: App|self.pool); +impl_has!(Selection: |self: App|self.project.selection); +impl_has!(Sequencer: |self: Track|self.sequencer); has_clips!( |self: App|self.pool.clips); impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); @@ -2433,11 +2407,11 @@ mod midi { parse_midi_input(self.port().iter(scope)) } } - impl>> HasMidiIns for T { + impl>> HasMidiIns for T { fn midi_ins (&self) -> &Vec { self.get() } fn midi_ins_mut (&mut self) -> &mut Vec { self.get_mut() } } - impl>> HasMidiOuts for T { + impl>> HasMidiOuts for T { fn midi_outs (&self) -> &Vec { self.get() } fn midi_outs_mut (&mut self) -> &mut Vec { self.get_mut() } } @@ -2653,13 +2627,24 @@ mod audio { #[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()); + impl_as_ref!(Vec: |self: Arrangement| self.tracks); + impl_as_mut!(Vec: |self: Arrangement| self.tracks); + + #[cfg(all(feature = "select"))] impl_as_ref!(Option: |self: App| self.project.as_ref()); + #[cfg(all(feature = "select"))] impl_as_mut!(Option: |self: App| self.project.as_mut()); + + 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> HasTrack for T { + impl>> HasTrack for T { fn track (&self) -> Option<&Track> { self.get() } fn track_mut (&mut self) -> Option<&mut Track> { self.get_mut() } } @@ -2744,11 +2729,48 @@ mod audio { None } } + + pub fn per_track <'a, T: Content + '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)))) + } + + pub fn per_track_top <'a, T: Content + '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, + 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( + track.color.lightest.rgb, + track.color.base.rgb, + callback(index, track))))}))) + } } #[cfg(feature = "scene")] mod scene { use crate::*; + impl AddScene for T {} + impl> + AsMut> + Send + Sync> HasScenes for T {} + impl> + AsMut> + Send + Sync> HasScene for T {} + + impl_as_ref!(Vec: |self: App| self.project.as_ref()); + impl_as_mut!(Vec: |self: App| self.project.as_mut()); + impl_as_ref!(Vec: |self: Arrangement| self.scenes.as_ref()); + impl_as_mut!(Vec: |self: Arrangement| self.scenes.as_mut()); + #[cfg(all(feature = "select"))] impl_as_ref!(Option: |self: App| self.project.as_ref()); + #[cfg(all(feature = "select"))] impl_as_mut!(Option: |self: App| self.project.as_mut()); + #[cfg(all(feature = "select"))] impl_as_ref!(Option: |self: Arrangement| self.selected.scene()); + #[cfg(all(feature = "select"))] impl_as_mut!(Option: |self: Arrangement| self.selected.scene()); + + 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 } @@ -2792,10 +2814,9 @@ mod audio { #[cfg(feature = "sequencer")] mod sequencer { use crate::*; - impl> HasSequencer for T { - fn sequencer (&self) -> &Sequencer { self.get() } - fn sequencer_mut (&mut self) -> &mut Sequencer { self.get_mut() } - } + + impl + AsMut> HasSequencer for T {} + impl Default for Sequencer { fn default () -> Self { Self { @@ -2816,10 +2837,12 @@ mod audio { } } } + 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") @@ -2829,6 +2852,7 @@ mod audio { .finish() } } + impl MidiMonitor for Sequencer { fn notes_in (&self) -> &Arc> { &self.notes_in @@ -2840,6 +2864,7 @@ mod audio { &mut self.monitoring } } + impl MidiRecord for Sequencer { fn recording (&self) -> bool { self.recording @@ -2854,6 +2879,7 @@ mod audio { &mut self.overdub } } + #[cfg(feature="clip")] impl HasPlayClip for Sequencer { fn reset (&self) -> bool { self.reset @@ -2874,6 +2900,7 @@ mod audio { &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 { @@ -2884,6 +2911,7 @@ mod audio { } } } + impl Sequencer { pub fn new ( name: impl AsRef, @@ -3536,6 +3564,55 @@ mod audio { } }) } + + 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 TuiOut, 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 { @@ -3680,6 +3757,17 @@ mod audio { } } + + fn draw_header (state: &Lv2, to: &mut TuiOut, 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 { diff --git a/app/tek_trait.rs b/app/tek_trait.rs index 4e7954ab..a7887992 100644 --- a/app/tek_trait.rs +++ b/app/tek_trait.rs @@ -115,10 +115,6 @@ pub trait JackPerfModel { } -//pub trait MaybeHas: Send + Sync { - //fn get (&self) -> Option<&T>; -//} - pub trait HasN: Send + Sync { fn get_nth (&self, key: usize) -> &T; fn get_nth_mut (&mut self, key: usize) -> &mut T; @@ -242,37 +238,44 @@ pub trait MidiRange: TimeRange + NoteRange {} /// can be clocked in microseconds with f64 without losing precision. pub trait TimeUnit: InteriorMutable {} -pub trait HasSceneScroll: HasScenes { - fn scene_scroll (&self) -> usize; -} - -pub trait HasTrackScroll: HasTracks { - fn track_scroll (&self) -> usize; -} +pub trait HasClipsSize { fn clips_size (&self) -> &Measure; } pub trait HasMidiClip { fn clip (&self) -> Option>>; } - -pub trait HasClipsSize { - fn clips_size (&self) -> &Measure; +pub trait HasClock: AsRef + AsMut { + fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } + fn clock (&self) -> &Clock { self.as_ref() } } - -pub trait HasClock: Send + Sync { - fn clock (&self) -> &Clock; - fn clock_mut (&mut self) -> &mut Clock; +pub trait HasDevices: AsRef> + AsMut> { + fn devices_mut (&mut self) -> &mut Vec { self.as_mut() } + fn devices (&self) -> &Vec { self.as_reF() } } - -pub trait HasDevices { - fn devices (&self) -> &Vec; - fn devices_mut (&mut self) -> &mut Vec; +pub trait HasSelection: AsRef + AsMut { + fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } + fn selection (&self) -> &Selection { self.as_ref() } } - -pub trait HasSelection: Has { - fn selection (&self) -> &Selection { self.get() } - fn selection_mut (&mut self) -> &mut Selection { self.get_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 HasScene: AsRef> + AsMut> { + fn scene_mut (&mut self) -> &mut Option { self.as_mut() } + fn scene (&self) -> Option<&Scene> { self.as_ref() } +} +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) } +} +pub trait HasSceneScroll: HasScenes { + fn scene_scroll (&self) -> usize; +} +pub trait HasTrackScroll: HasTracks { + fn track_scroll (&self) -> usize; } - pub trait HasWidth { const MIN_WIDTH: usize; /// Increment track width. @@ -280,48 +283,17 @@ pub trait HasWidth { /// 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>>; } -pub trait HasSequencer { - fn sequencer (&self) -> &Sequencer; - fn sequencer_mut (&mut self) -> &mut Sequencer; -} - -pub trait HasScene: Has> + Send + Sync { - fn scene (&self) -> Option<&Scene> { - Has::>::get(self).as_ref() - } - fn scene_mut (&mut self) -> &mut Option { - Has::>::get_mut(self) - } -} - -pub trait HasScenes: Has> + Send + Sync { - fn scenes (&self) -> &Vec { - Has::>::get(self) - } - fn scenes_mut (&mut self) -> &mut Vec { - Has::>::get_mut(self) - } - /// 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) - } -} - /// ``` /// use tek::{MidiEditor, HasEditor, tengri::Has}; /// /// let mut host = TestEditorHost(Some(MidiEditor::default())); /// struct TestEditorHost(Option); -/// impl Has> for TestEditorHost { +/// impl AsRef> for TestEditorHost { /// fn get (&self) -> &Option { &self.0 } /// fn get_mut (&mut self) -> &mut Option { &mut self.0 } /// } @@ -332,7 +304,7 @@ pub trait HasScenes: Has> + Send + Sync { /// let _ = host.editor_w(); /// let _ = host.editor_h(); /// ``` -pub trait HasEditor: Has> { +pub trait HasEditor: AsRef> { fn editor (&self) -> Option<&MidiEditor> { self.get().as_ref() } @@ -407,9 +379,10 @@ pub trait HasMidiOuts { } } -pub trait HasTracks: Has> + Send + Sync { - fn tracks (&self) -> &Vec { Has::>::get(self) } - fn tracks_mut (&mut self) -> &mut Vec { Has::>::get_mut(self) } +impl> + AsMut> + Send + Sync> HasTracks for T {} +pub trait HasTracks: AsRef> + AsMut> + Send + Sync { + 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() { diff --git a/dizzle b/dizzle index 7d1fbe3f..909b94cb 160000 --- a/dizzle +++ b/dizzle @@ -1 +1 @@ -Subproject commit 7d1fbe3fbe53699a3e12eb5a3d55db79653d72d8 +Subproject commit 909b94cbd4beffb49be314724dc79db9374bcc99 From 6295f2e601e9762b26e48bc49062b57feaf215b2 Mon Sep 17 00:00:00 2001 From: okay stopped screaming Date: Mon, 23 Feb 2026 21:21:17 +0200 Subject: [PATCH 2/4] move the jack stuff into tengri --- .gitmodules | 4 +- Cargo.lock | 3 +- Cargo.toml | 2 - app/Cargo.toml | 0 app/tek.rs | 22 +--- app/tek_impls.rs | 311 +++++++++++++++------------------------------- app/tek_struct.rs | 52 -------- app/tek_trait.rs | 123 +----------------- app/tek_type.rs | 26 ---- dizzle | 1 - rust-jack | 1 - tengri | 2 +- 12 files changed, 107 insertions(+), 440 deletions(-) delete mode 100644 app/Cargo.toml delete mode 160000 dizzle delete mode 160000 rust-jack diff --git a/.gitmodules b/.gitmodules index 50fe8c6d..04bb5796 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,8 +6,8 @@ path = tengri url = https://codeberg.org/unspeaker/tengri [submodule "deps/rust-jack"] - path = rust-jack + path = tengri/rust-jack url = https://codeberg.org/unspeaker/rust-jack [submodule "deps/dizzle"] - path = dizzle + path = tengri/dizzle url = ssh://git@codeberg.org/unspeaker/dizzle.git diff --git a/Cargo.lock b/Cargo.lock index 50c81b5a..a1080f3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2992,9 +2992,7 @@ dependencies = [ "builder-pattern", "bumpalo", "clap", - "dizzle", "gtk", - "jack", "konst", "livi", "midly", @@ -3035,6 +3033,7 @@ dependencies = [ "better-panic", "crossterm 0.29.0", "dizzle", + "jack", "palette", "quanta", "rand 0.8.5", diff --git a/Cargo.toml b/Cargo.toml index 43c3ded8..f4e2a106 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,6 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"] [dependencies] tengri = { path = "./tengri", features = [ "tui", "dsl" ] } -dizzle = { path = "./dizzle" } -jack = { path = "./rust-jack" } ansi_term = { version = "0.12.1" } atomic_float = { version = "1.0.0" } diff --git a/app/Cargo.toml b/app/Cargo.toml deleted file mode 100644 index e69de29b..00000000 diff --git a/app/tek.rs b/app/tek.rs index 8cd455fc..178070da 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -13,8 +13,8 @@ extern crate xdg; pub(crate) use ::xdg::BaseDirectories; pub extern crate atomic_float; pub(crate) use atomic_float::AtomicF64; -pub extern crate jack; -pub(crate) use ::jack::{*, contrib::{*, ClosureProcessHandler}}; +//pub extern crate jack; +//pub(crate) use ::jack::{*, contrib::{*, ClosureProcessHandler}}; pub extern crate midly; pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; pub extern crate tengri; @@ -301,10 +301,10 @@ pub fn tek_print_status (project: &Arrangement) { } /// Return boxed iterator of MIDI events -pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>) +pub fn parse_midi_input <'a> (input: ::tengri::jack::MidiIter<'a>) -> Box, &'a [u8])> + 'a> { - Box::new(input.map(|::jack::RawMidi { time, bytes }|( + Box::new(input.map(|::tengri::jack::RawMidi { time, bytes }|( time as usize, LiveEvent::parse(bytes).unwrap(), bytes @@ -426,24 +426,10 @@ impl Device { } } -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 - } -}); //take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); - #[macro_export] macro_rules! has_clip { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? { diff --git a/app/tek_impls.rs b/app/tek_impls.rs index 12987408..53ec57cc 100644 --- a/app/tek_impls.rs +++ b/app/tek_impls.rs @@ -280,80 +280,8 @@ mod app { } } } -} -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!(App: tek_jack_process, tek_jack_event); } #[cfg(feature = "vst2")] impl ::vst::host::Host for Plugin {} @@ -1703,8 +1631,6 @@ impl AsRef> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem] impl ClipsView for T {} impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.size_inner } } -impl<'j> HasJack<'j> for Jack<'j> { fn jack (&self) -> &Jack<'j> { self } } -impl<'j> HasJack<'j> for &Jack<'j> { fn jack (&self) -> &Jack<'j> { self } } 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 } } @@ -1735,8 +1661,6 @@ impl> RegisterPorts for J { #[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_ins); #[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_outs); -audio!(App: tek_jack_process, tek_jack_event); - impl_has!(Jack<'static>: |self: App|self.jack); impl_has!(Dialog: |self: App|self.dialog); impl_has!(Measure: |self: App|self.size); @@ -1838,141 +1762,6 @@ namespace!(App: Option>> { }; }); - -mod draw { - - use crate::*; - - impl Draw for MidiEditor { - fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } - } - - impl Draw for PianoHorizontal { - fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } - } - -} - -mod jack { - use crate::*; - - /// Implement [Jack] constructor and methods - impl<'j> Jack<'j> { - /// Register new [Client] and wrap it for shared use. - pub fn new_run + Audio + Send + Sync + 'static> ( - name: &impl AsRef, - init: impl FnOnce(Jack<'j>)->Usually - ) -> Usually>> { - Jack::new(name)?.run(init) - } - - pub fn new (name: &impl AsRef) -> Usually { - let client = Client::new(name.as_ref(), ClientOptions::NO_START_SERVER)?.0; - Ok(Jack(Arc::new(RwLock::new(JackState::Inactive(client))))) - } - - pub fn run + Audio + Send + Sync + 'static> - (self, init: impl FnOnce(Self)->Usually) -> Usually>> - { - let client_state = self.0.clone(); - let app: Arc> = Arc::new(RwLock::new(init(self)?)); - let mut state = Activating; - std::mem::swap(&mut*client_state.write().unwrap(), &mut state); - if let Inactive(client) = state { - // This is the misc notifications handler. It's a struct that wraps a [Box] - // which performs type erasure on a callback that takes [JackEvent], which is - // one of the available misc notifications. - let notify = JackNotify(Box::new({ - let app = app.clone(); - move|event|(&mut*app.write().unwrap()).handle(event) - }) as BoxedJackEventHandler); - // This is the main processing handler. It's a struct that wraps a [Box] - // which performs type erasure on a callback that takes [Client] and [ProcessScope] - // and passes them down to the `app`'s `process` callback, which in turn - // implements audio and MIDI input and output on a realtime basis. - let process = ClosureProcessHandler::new(Box::new({ - let app = app.clone(); - move|c: &_, s: &_|if let Ok(mut app) = app.write() { - app.process(c, s) - } else { - Control::Quit - } - }) as BoxedAudioHandler); - // Launch a client with the two handlers. - *client_state.write().unwrap() = Active( - client.activate_async(notify, process)? - ); - } else { - unreachable!(); - } - Ok(app) - } - - /// Run something with the client. - pub fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { - match &*self.0.read().unwrap() { - Inert => panic!("jack client not activated"), - Inactive(client) => op(client), - Activating => panic!("jack client has not finished activation"), - Active(client) => op(client.as_client()), - } - } - } - - impl NotificationHandler for JackNotify { - fn thread_init(&self, _: &Client) { - self.0(JackEvent::ThreadInit); - } - unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { - self.0(JackEvent::Shutdown(status, reason.into())); - } - fn freewheel(&mut self, _: &Client, enabled: bool) { - self.0(JackEvent::Freewheel(enabled)); - } - fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { - self.0(JackEvent::SampleRate(frames)); - Control::Quit - } - fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { - self.0(JackEvent::ClientRegistration(name.into(), reg)); - } - fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { - self.0(JackEvent::PortRegistration(id, reg)); - } - fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { - self.0(JackEvent::PortRename(id, old.into(), new.into())); - Control::Continue - } - fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { - self.0(JackEvent::PortsConnected(a, b, are)); - } - fn graph_reorder(&mut self, _: &Client) -> Control { - self.0(JackEvent::GraphReorder); - Control::Continue - } - fn xrun(&mut self, _: &Client) -> Control { - self.0(JackEvent::XRun); - Control::Continue - } - } - - impl JackPerfModel for PerfModel { - fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope) { - if let Some(t0) = t0 { - let t1 = self.clock.raw(); - self.used.store( - self.clock.delta_as_nanos(t0, t1) as f64, - Relaxed, - ); - self.window.store( - scope.cycle_times().unwrap().period_usecs as f64, - Relaxed, - ); - } - } - } -} - mod time { use crate::*; impl Moment { @@ -3062,6 +2851,12 @@ mod audio { } } } + impl Draw for MidiEditor { + fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } + } + impl Draw for PianoHorizontal { + fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } + } } #[cfg(feature = "sampler")] mod sampler { @@ -3565,7 +3360,7 @@ mod audio { }) } - audio!(Sampler: sampler_jack_process); + 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) { @@ -3619,7 +3414,7 @@ mod audio { use crate::*; - audio!(Lv2: lv2_jack_process); + impl_audio!(Lv2: lv2_jack_process); impl Lv2 { const INPUT_BUFFER: usize = 1024; @@ -4246,3 +4041,91 @@ mod config { } } } + +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/app/tek_struct.rs b/app/tek_struct.rs index b4e0ea4b..bf1fa768 100644 --- a/app/tek_struct.rs +++ b/app/tek_struct.rs @@ -2,58 +2,6 @@ use crate::*; use clap::{self, Parser, Subcommand}; use builder_pattern::Builder; -/// Wraps [JackState], and through it [jack::Client] when connected. -/// -/// ``` -/// let jack = tek::Jack::default(); -/// ``` -#[derive(Clone, Debug, Default)] pub struct Jack<'j> ( - pub(crate) Arc>> -); - -/// This is a connection which may be [Inactive], [Activating], or [Active]. -/// In the [Active] and [Inactive] states, [JackState::client] returns a -/// [jack::Client], which you can use to talk to the JACK API. -/// -/// ``` -/// let state = tek::JackState::default(); -/// ``` -#[derive(Debug, Default)] pub enum JackState<'j> { - /// Unused - #[default] Inert, - /// Before activation. - Inactive(Client), - /// During activation. - Activating, - /// After activation. Must not be dropped for JACK thread to persist. - Active(DynamicAsyncClient<'j>), -} - -/// Event enum for JACK events. -/// -/// ``` -/// let event = tek::JackEvent::XRun; -/// ``` -#[derive(Debug, Clone, PartialEq)] pub enum JackEvent { - ThreadInit, - Shutdown(ClientStatus, Arc), - Freewheel(bool), - SampleRate(Frames), - ClientRegistration(Arc, bool), - PortRegistration(PortId, bool), - PortRename(PortId, Arc, Arc), - PortsConnected(PortId, PortId, bool), - GraphReorder, - XRun, -} - -/// Generic notification handler that emits [JackEvent] -/// -/// ``` -/// let notify = tek::JackNotify(|_|{}); -/// ``` -pub struct JackNotify(pub T); - /// Total state /// /// ``` diff --git a/app/tek_trait.rs b/app/tek_trait.rs index a7887992..4e738b82 100644 --- a/app/tek_trait.rs +++ b/app/tek_trait.rs @@ -1,125 +1,6 @@ use crate::*; use std::sync::atomic::Ordering; -/// Things that can provide a [jack::Client] reference. -/// -/// ``` -/// use tek::{Jack, HasJack}; -/// -/// let jack: &Jack = Jacked::default().jack(); -/// -/// #[derive(Default)] struct Jacked<'j>(Jack<'j>); -/// -/// impl<'j> tek::HasJack<'j> for Jacked<'j> { -/// fn jack (&self) -> &Jack<'j> { &self.0 } -/// } -/// ``` -pub trait HasJack<'j>: Send + Sync { - - /// Return the internal [jack::Client] handle - /// that lets you call the JACK API. - fn jack (&self) -> &Jack<'j>; - - fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { - self.jack().with_client(op) - } - - fn port_by_name (&self, name: &str) -> Option> { - self.with_client(|client|client.port_by_name(name)) - } - - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - - fn register_port (&self, name: impl AsRef) -> Usually> { - self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?)) - } - - fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->Position) -> Usually<()> { - if enable { - self.with_client(|client|match client.register_timebase_callback(false, callback) { - Ok(_) => Ok(()), - Err(e) => Err(e) - })? - } - Ok(()) - } - - fn sync_follow (&self, _enable: bool) -> Usually<()> { - // TODO: sync follow - Ok(()) - } -} - -/// Trait for thing that has a JACK process callback. -pub trait Audio { - - /// Handle a JACK event. - fn handle (&mut self, _event: JackEvent) {} - - /// Projecss a JACK chunk. - fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { - Control::Continue - } - - /// The JACK process callback function passed to the server. - fn callback ( - state: &Arc>, client: &Client, scope: &ProcessScope - ) -> Control where Self: Sized { - if let Ok(mut state) = state.write() { - state.process(client, scope) - } else { - Control::Quit - } - } - -} - -/// Implement [Audio]: provide JACK callbacks. -#[macro_export] macro_rules! audio { - - (| - $self1:ident: - $Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident - |$cb:expr$(;|$self2:ident,$e:ident|$cb2:expr)?) => { - impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { - #[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb } - $(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })? - } - }; - - ($Struct:ident: $process:ident, $handle:ident) => { - impl Audio for $Struct { - #[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { - $process(self, c, s) - } - #[inline] fn handle (&mut self, e: JackEvent) { - $handle(self, e) - } - } - }; - - ($Struct:ident: $process:ident) => { - impl Audio for $Struct { - #[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { - $process(self, c, s) - } - } - }; - -} - -pub trait JackPerfModel { - fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope); -} - - -pub trait HasN: Send + Sync { - fn get_nth (&self, key: usize) -> &T; - fn get_nth_mut (&mut self, key: usize) -> &mut T; -} - pub trait Gettable { /// Returns current value fn get (&self) -> T; @@ -249,7 +130,7 @@ pub trait HasClock: AsRef + AsMut { } pub trait HasDevices: AsRef> + AsMut> { fn devices_mut (&mut self) -> &mut Vec { self.as_mut() } - fn devices (&self) -> &Vec { self.as_reF() } + fn devices (&self) -> &Vec { self.as_ref() } } pub trait HasSelection: AsRef + AsMut { fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } @@ -264,7 +145,7 @@ pub trait HasScene: AsRef> + AsMut> { fn scene (&self) -> Option<&Scene> { self.as_ref() } } pub trait HasScenes: AsRef> + AsMut> { - fn scenes (&self) -> &Vec { self.as_reF() } + 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() } diff --git a/app/tek_type.rs b/app/tek_type.rs index bcc90fb8..fd2c7d9d 100644 --- a/app/tek_type.rs +++ b/app/tek_type.rs @@ -1,31 +1,5 @@ use crate::*; -/// Running JACK [AsyncClient] with maximum type erasure. -/// -/// One [Box] contains function that handles [JackEvent]s. -/// -/// Another [Box] containing a function that handles realtime IO. -/// -/// That's all it knows about them. -pub type DynamicAsyncClient<'j> - = AsyncClient, DynamicAudioHandler<'j>>; - -/// Notification handler wrapper for [BoxedAudioHandler]. -pub type DynamicAudioHandler<'j> = - ClosureProcessHandler<(), BoxedAudioHandler<'j>>; - -/// Boxed realtime callback. -pub type BoxedAudioHandler<'j> = - Box Control + Send + Sync + 'j>; - -/// Notification handler wrapper for [BoxedJackEventHandler]. -pub type DynamicNotifications<'j> = - JackNotify>; - -/// Boxed [JackEvent] callback. -pub type BoxedJackEventHandler<'j> = - Box; - pub type MidiData = Vec>; diff --git a/dizzle b/dizzle deleted file mode 160000 index 909b94cb..00000000 --- a/dizzle +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 909b94cbd4beffb49be314724dc79db9374bcc99 diff --git a/rust-jack b/rust-jack deleted file mode 160000 index 764a38a8..00000000 --- a/rust-jack +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 764a38a880ab4749ea60aa7e53cd814b858e606c diff --git a/tengri b/tengri index 5d0dc40f..c1b727ba 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 5d0dc40fdcd7cc022d1e468d9bf59de722949ace +Subproject commit c1b727bafc27dc715d7239a0bc63a1cdfe962bc2 From ab90129f4c44d85ed0d3809bd25b5abb800484c3 Mon Sep 17 00:00:00 2001 From: okay stopped screaming Date: Mon, 23 Feb 2026 22:49:20 +0200 Subject: [PATCH 3/4] use new AsRefOpt/AsMutOpt glue traits --- Cargo.toml | 1 - Justfile | 2 +- app/tek.rs | 3 +- app/tek_impls.rs | 1237 +++++++++++++++++++++------------------------ app/tek_struct.rs | 2 +- app/tek_trait.rs | 154 +++--- tengri | 2 +- 7 files changed, 661 insertions(+), 740 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f4e2a106..1a05aaf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,6 @@ proptest-derive = { version = "^0.5.1" } [features] default = ["cli", "arranger", "sampler"] - arranger = ["port", "editor", "sequencer", "track", "scene", "clip", "select"] browse = [] clap = [] diff --git a/Justfile b/Justfile index ebb68b1c..21326baa 100644 --- a/Justfile +++ b/Justfile @@ -2,7 +2,7 @@ export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg export RUST_BACKTRACE := "1" default +ARGS="new": - target/debug/tek {{ARGS}} + cargo run -- {{ARGS}} doc +ARGS="": cargo doc --open -j4 --document-private-items {{ARGS}} diff --git a/app/tek.rs b/app/tek.rs index 178070da..6b2d9963 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -59,7 +59,6 @@ pub(crate) use tengri::{ pub(crate) use ConnectName::*; pub(crate) use ConnectScope::*; pub(crate) use ConnectStatus::*; -pub(crate) use JackState::*; /// Command-line entrypoint. #[cfg(feature = "cli")] pub fn main () -> Usually<()> { @@ -70,7 +69,7 @@ pub(crate) use JackState::*; /// Create a new application from a backend, project, config, and mode /// /// ``` -/// let jack = tek::Jack::new(&"test_tek").expect("failed to connect to jack"); +/// let jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack"); /// let proj = tek::Arrangement::default(); /// let mut conf = tek::Config::default(); /// conf.add("(mode hello)"); diff --git a/app/tek_impls.rs b/app/tek_impls.rs index 53ec57cc..71cca19c 100644 --- a/app/tek_impls.rs +++ b/app/tek_impls.rs @@ -1,9 +1,35 @@ use crate::*; use std::fmt::Write; -use std::path::PathBuf; + +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_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!(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); + has_clips!( |self: App|self.pool.clips); + impl_audio!(App: tek_jack_process, tek_jack_event); + impl_as_ref!(Vec: |self: App| self.project.as_ref()); + impl_as_mut!(Vec: |self: App| self.project.as_mut()); impl Draw for App { fn draw (&self, to: &mut TuiOut) { @@ -280,8 +306,6 @@ mod app { } } } - - impl_audio!(App: tek_jack_process, tek_jack_event); } #[cfg(feature = "vst2")] impl ::vst::host::Host for Plugin {} @@ -289,19 +313,20 @@ mod app { mod arrange { use crate::*; - impl_has!(Jack<'static>: |self: Arrangement|self.jack); - impl_has!(Measure: |self: Arrangement|self.size); - - #[cfg(feature = "editor")] impl_has!(Option: |self: Arrangement|self.editor); - #[cfg(feature = "port")] impl_has!(Vec: |self: Arrangement|self.midi_ins); - #[cfg(feature = "port")] impl_has!(Vec: |self: Arrangement|self.midi_outs); - #[cfg(feature = "clock")] impl_has!(Clock: |self: Arrangement|self.clock); - #[cfg(feature = "select")] impl_has!(Selection: |self: Arrangement|self.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); + #[cfg(feature = "port")] impl_has!(Vec: |self: Arrangement| self.midi_ins); + #[cfg(feature = "port")] impl_has!(Vec: |self: Arrangement| self.midi_outs); + #[cfg(feature = "clock")] impl_has!(Clock: |self: Arrangement| self.clock); + #[cfg(feature = "select")] impl_has!(Selection: |self: Arrangement| self.selection); + #[cfg(feature = "select")] impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); + #[cfg(feature = "select")] impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); + #[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); + #[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); #[cfg(feature = "select")] impl Arrangement { - #[cfg(feature = "clip")] fn selected_clip (&self) -> Option { todo!() } - #[cfg(feature = "scene")] fn selected_scene (&self) -> Option { todo!() } - #[cfg(feature = "track")] fn selected_track (&self) -> Option { todo!() } #[cfg(feature = "port")] fn selected_midi_in (&self) -> Option { todo!() } #[cfg(feature = "port")] fn selected_midi_out (&self) -> Option { todo!() } fn selected_device (&self) -> Option { todo!() } @@ -349,16 +374,6 @@ mod arrange { 2 //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } - /// Get the active track - #[cfg(feature = "track")] pub fn get_track (&self) -> Option<&Track> { - let index = self.selection().track()?; - Has::>::get(self).get(index) - } - /// Get a mutable reference to the active track - #[cfg(feature = "track")] pub fn get_track_mut (&mut self) -> Option<&mut Track> { - let index = self.selection().track()?; - Has::>::get_mut(self).get_mut(index) - } /// Add multiple tracks #[cfg(feature = "track")] pub fn tracks_add ( &mut self, @@ -501,20 +516,6 @@ mod arrange { Align::nw(format!(" ยท {}", "--"))))))))); })) } - /// Get the active scene - #[cfg(feature = "scene")] pub fn get_scene (&self) -> Option<&Scene> { - let index = self.selection().scene()?; - Has::>::get(self).get(index) - } - /// Get a mutable reference to the active scene - #[cfg(feature = "scene")] pub fn get_scene_mut (&mut self) -> Option<&mut Scene> { - let index = self.selection().scene()?; - Has::>::get_mut(self).get_mut(index) - } - /// Get the active clip - #[cfg(feature = "clip")] pub fn get_clip (&self) -> Option>> { - self.get_scene()?.clips.get(self.selection().track()?)?.clone() - } /// Put a clip in a slot #[cfg(feature = "clip")] pub fn clip_put ( &mut self, track: usize, scene: usize, clip: Option>> @@ -537,17 +538,17 @@ mod arrange { } /// Toggle looping for the active clip #[cfg(feature = "clip")] pub fn toggle_loop (&mut self) { - if let Some(clip) = self.get_clip() { + 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.get_track()?.sampler(0) + 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.get_track_mut()?.sampler_mut(0) + self.selected_track_mut()?.sampler_mut(0) } } @@ -672,7 +673,6 @@ mod browse { //take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -impl + AsMut> HasClock for T {} impl std::fmt::Debug for Clock { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { @@ -950,462 +950,6 @@ impl ClockView { //} } - -impl>> HasEditor for T {} - -impl_has!(Measure: |self: MidiEditor|self.size); - - -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() - } -} - -from!(MidiEditor: |clip: &Arc>| { - let model = Self::from(Some(clip.clone())); - model.redraw(); - model -}); - -from!(MidiEditor: |clip: Option>>| { - let mut model = Self::default(); - *model.clip_mut() = clip; - model.redraw(); - model -}); - -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 Content + '_ { - 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( - button_2("f2", "name ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), - Fill::X(Align::w(Bsp::e( - button_2("l", "ength ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), - Fill::X(Align::w(Bsp::e( - button_2("r", "epeat ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), - )) - } - pub fn edit_status (&self) -> impl Content + '_ { - 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()); - Fixed::X(20, col!( - Fill::X(Align::w(Bsp::e( - button_2("t", "ime ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), - format!("{length} /{time_zoom} +{time_pos} "))))))), - Fill::X(Align::w(Bsp::e( - button_2("z", "lock ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), - format!("{time_lock}"))))))), - Fill::X(Align::w(Bsp::e( - button_2("x", "note ", false), - Fill::X(Align::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 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_has!(Measure:|self:PianoHorizontal|self.size); - -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 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 { - /// 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 Content { - 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|{ - 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 Content { - 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 TuiOut|{ - 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 Content { - 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()); - Fill::Y(Fixed::X(self.keys_width, Thunk::new(move|to: &mut TuiOut|{ - 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 Content + '_ { - Fill::X(Fixed::Y(1, Thunk::new(move|to: &mut TuiOut|{ - 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 HasContent for OctaveVertical { - fn content (&self) -> impl Content { - row!( - 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 Layout for RmsMeter {} -impl Layout for Log10Meter {} - impl Connect { pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) -> Vec @@ -1555,7 +1099,6 @@ impl Selection { } -impl + AsMut> HasSelection for T {} /// Default is always empty map regardless if `E` and `C` implement [Default]. impl Default for Bind { @@ -1628,9 +1171,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 ClipsView for T {} 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 } } @@ -1652,23 +1193,7 @@ impl> RegisterPorts for J { } } -#[cfg(feature = "clock")] impl_has!(Clock: |self: App|self.project.clock); #[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock); -#[cfg(feature = "clock")] impl_has!(Clock: |self: Sequencer|self.clock); - -#[cfg(feature = "port")] impl_has!(Vec: |self: App|self.project.midi_ins); -#[cfg(feature = "port")] impl_has!(Vec: |self: App|self.project.midi_outs); -#[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_ins); -#[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_outs); - -impl_has!(Jack<'static>: |self: App|self.jack); -impl_has!(Dialog: |self: App|self.dialog); -impl_has!(Measure: |self: App|self.size); -impl_has!(Option: |self: App|self.project.editor); -impl_has!(Pool: |self: App|self.pool); -impl_has!(Selection: |self: App|self.project.selection); -impl_has!(Sequencer: |self: Track|self.sequencer); -has_clips!( |self: App|self.pool.clips); impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); impl_debug!(Condition |self, w| { write!(w, "*") }); @@ -2015,6 +1540,15 @@ mod time { 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 NotePoint for MidiCursor { fn note_len (&self) -> &AtomicUsize { @@ -2031,20 +1565,6 @@ mod midi { } } - impl MidiPoint for T {} - - 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 MidiRange for T {} - impl TimeRange for MidiSelection { fn time_len (&self) -> &AtomicUsize { &self.time_len } fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } @@ -2196,13 +1716,13 @@ mod midi { parse_midi_input(self.port().iter(scope)) } } - impl>> HasMidiIns for T { - fn midi_ins (&self) -> &Vec { self.get() } - fn midi_ins_mut (&mut self) -> &mut Vec { self.get_mut() } + 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>> HasMidiOuts for T { - fn midi_outs (&self) -> &Vec { self.get() } - fn midi_outs_mut (&mut self) -> &mut Vec { self.get_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<()> { @@ -2282,7 +1802,7 @@ mod midi { controller: 123.into(), value: 0.into() }]]), - Some(ItemColor::from_rgb(Color::Rgb(32, 32, 32)).into()) + Some(ItemColor::from_tui(Color::Rgb(32, 32, 32)).into()) ) } } @@ -2375,6 +1895,9 @@ mod audio { mod meter { use crate::*; + impl Layout for RmsMeter {} + impl Layout for Log10Meter {} + impl Draw for RmsMeter { fn draw (&self, to: &mut TuiOut) { let XYWH(x, y, w, h) = to.area(); @@ -2415,15 +1938,10 @@ mod audio { #[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()); - impl_as_ref!(Vec: |self: Arrangement| self.tracks); - impl_as_mut!(Vec: |self: Arrangement| self.tracks); - - #[cfg(all(feature = "select"))] impl_as_ref!(Option: |self: App| self.project.as_ref()); - #[cfg(all(feature = "select"))] impl_as_mut!(Option: |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 } } @@ -2433,11 +1951,6 @@ mod audio { fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } } - impl>> HasTrack for T { - fn track (&self) -> Option<&Track> { self.get() } - fn track_mut (&mut self) -> Option<&mut Track> { self.get_mut() } - } - impl Track { /// Create a new track with only the default [Sequencer]. pub fn new ( @@ -2542,35 +2055,18 @@ mod audio { #[cfg(feature = "scene")] mod scene { use crate::*; - - impl AddScene for T {} - impl> + AsMut> + Send + Sync> HasScenes for T {} - impl> + AsMut> + Send + Sync> HasScene for T {} - - impl_as_ref!(Vec: |self: App| self.project.as_ref()); - impl_as_mut!(Vec: |self: App| self.project.as_mut()); - impl_as_ref!(Vec: |self: Arrangement| self.scenes.as_ref()); - impl_as_mut!(Vec: |self: Arrangement| self.scenes.as_mut()); - #[cfg(all(feature = "select"))] impl_as_ref!(Option: |self: App| self.project.as_ref()); - #[cfg(all(feature = "select"))] impl_as_mut!(Option: |self: App| self.project.as_mut()); - #[cfg(all(feature = "select"))] impl_as_ref!(Option: |self: Arrangement| self.selected.scene()); - #[cfg(all(feature = "select"))] impl_as_mut!(Option: |self: Arrangement| self.selected.scene()); - + #[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 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 { - 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!() } /// Returns the pulse length of the longest clip in the scene pub fn pulses (&self) -> usize { self.clips.iter().fold(0, |a, p|{ @@ -2598,14 +2094,21 @@ mod audio { 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 + AsMut> HasSequencer for T {} - + impl_has!(Sequencer: |self: Track| self.sequencer); + #[cfg(feature = "clock")] impl_has!(Clock: |self: Sequencer|self.clock); + #[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_ins); + #[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_outs); + impl_has!(Measure: |self: MidiEditor| self.size); + impl_has!(Measure: |self: PianoHorizontal| self.size); impl Default for Sequencer { fn default () -> Self { Self { @@ -2626,81 +2129,6 @@ mod audio { } } } - - 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 notes_in (&self) -> &Arc> { - &self.notes_in - } - fn monitoring (&self) -> bool { - self.monitoring - } - fn monitoring_mut (&mut self) -> &mut bool { - &mut self.monitoring - } - } - - 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 Sequencer { pub fn new ( name: impl AsRef, @@ -2851,6 +2279,56 @@ mod audio { } } } + 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 TuiOut) { self.content().draw(to) } } @@ -2859,12 +2337,457 @@ mod audio { } } +#[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 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 Content + '_ { + 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( + button_2("f2", "name ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), + Fill::X(Align::w(Bsp::e( + button_2("l", "ength ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), + Fill::X(Align::w(Bsp::e( + button_2("r", "epeat ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), + )) + } + pub fn edit_status (&self) -> impl Content + '_ { + 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()); + Fixed::X(20, col!( + Fill::X(Align::w(Bsp::e( + button_2("t", "ime ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + format!("{length} /{time_zoom} +{time_pos} "))))))), + Fill::X(Align::w(Bsp::e( + button_2("z", "lock ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + format!("{time_lock}"))))))), + Fill::X(Align::w(Bsp::e( + button_2("x", "note ", false), + Fill::X(Align::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 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 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 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 { + /// 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 Content { + 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|{ + 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 Content { + 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 TuiOut|{ + 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 Content { + 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()); + Fill::Y(Fixed::X(self.keys_width, Thunk::new(move|to: &mut TuiOut|{ + 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 Content + '_ { + Fill::X(Fixed::Y(1, Thunk::new(move|to: &mut TuiOut|{ + 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 HasContent for OctaveVertical { + fn content (&self) -> impl Content { + row!( + 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]) - } + fn default () -> Self { Self([const { None }; N]) } } impl Iterator for Voice { type Item = [f32;2]; @@ -3411,11 +3334,8 @@ mod audio { } #[cfg(feature = "lv2")] mod lv2 { - use crate::*; - impl_audio!(Lv2: lv2_jack_process); - impl Lv2 { const INPUT_BUFFER: usize = 1024; pub fn new ( @@ -3454,7 +3374,6 @@ mod audio { }) } } - fn lv2_jack_process ( Lv2 { midi_ins, midi_outs, audio_ins, audio_outs, @@ -3569,7 +3488,7 @@ mod pool { use crate::*; has_clips!(|self: Pool|self.clips); has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); - from!(Pool: |clip:&Arc>|{ + impl_from!(Pool: |clip:&Arc>|{ let model = Self::default(); model.clips.write().unwrap().push(clip.clone()); model.clip.store(1, Relaxed); diff --git a/app/tek_struct.rs b/app/tek_struct.rs index bf1fa768..34d2277f 100644 --- a/app/tek_struct.rs +++ b/app/tek_struct.rs @@ -5,7 +5,7 @@ use builder_pattern::Builder; /// Total state /// /// ``` -/// use tek::{TracksView, ScenesView, AddScene}; +/// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; /// let mut app = tek::App::default(); /// let _ = app.scene_add(None, None).unwrap(); /// let _ = app.update_clock(); diff --git a/app/tek_trait.rs b/app/tek_trait.rs index 4e738b82..2ee87476 100644 --- a/app/tek_trait.rs +++ b/app/tek_trait.rs @@ -125,24 +125,55 @@ pub trait HasMidiClip { fn clip (&self) -> Option>>; } pub trait HasClock: AsRef + AsMut { - fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } fn clock (&self) -> &Clock { self.as_ref() } + fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } } pub trait HasDevices: AsRef> + AsMut> { - fn devices_mut (&mut self) -> &mut Vec { self.as_mut() } fn devices (&self) -> &Vec { self.as_ref() } -} -pub trait HasSelection: AsRef + AsMut { - fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } - fn selection (&self) -> &Selection { 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 HasScene: AsRef> + AsMut> { - fn scene_mut (&mut self) -> &mut Option { self.as_mut() } - fn scene (&self) -> Option<&Scene> { 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() } @@ -150,12 +181,30 @@ pub trait HasScenes: AsRef> + AsMut> { /// 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) } -} -pub trait HasSceneScroll: HasScenes { - fn scene_scroll (&self) -> usize; -} -pub trait HasTrackScroll: HasTracks { - fn track_scroll (&self) -> usize; + /// 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; @@ -170,39 +219,26 @@ pub trait HasMidiBuffers { } /// ``` -/// use tek::{MidiEditor, HasEditor, tengri::Has}; +/// use tek::{*, tengri::*}; /// -/// let mut host = TestEditorHost(Some(MidiEditor::default())); -/// struct TestEditorHost(Option); -/// impl AsRef> for TestEditorHost { -/// fn get (&self) -> &Option { &self.0 } -/// fn get_mut (&mut self) -> &mut Option { &mut self.0 } -/// } +/// 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: AsRef> { - fn editor (&self) -> Option<&MidiEditor> { - self.get().as_ref() - } - fn editor_mut (&mut self) -> Option<&mut MidiEditor> { - self.get_mut().as_mut() - } - 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 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>; @@ -212,7 +248,6 @@ pub trait HasClips { (self.clips().len() - 1, clip) } } - /// Trait for thing that may receive MIDI. pub trait HasMidiIns { fn midi_ins (&self) -> &Vec; @@ -237,7 +272,6 @@ pub trait HasMidiIns { }) } } - /// Trait for thing that may output MIDI. pub trait HasMidiOuts { fn midi_outs (&self) -> &Vec; @@ -259,9 +293,7 @@ pub trait HasMidiOuts { } } } - -impl> + AsMut> + Send + Sync> HasTracks for T {} -pub trait HasTracks: AsRef> + AsMut> + Send + Sync { +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 @@ -292,10 +324,9 @@ pub trait HasTracks: AsRef> + AsMut> + Send + Sync { /// Spacing between tracks. const TRACK_SPACING: usize = 0; } - -pub trait HasTrack { - fn track (&self) -> Option<&Track>; - fn track_mut (&mut self) -> Option<&mut Track>; +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 { self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) } @@ -523,33 +554,6 @@ pub trait MidiViewer: Measured + MidiRange + MidiPoint + Debug + Send + } } -pub trait AddScene: HasScenes + HasTracks { - /// Add multiple scenes - fn scenes_add (&mut self, n: usize) -> Usually<()> { - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..n { - let _ = 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)> - { - 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 ClipsView: TracksView + ScenesView { fn view_scenes_clips <'a> (&'a self) diff --git a/tengri b/tengri index c1b727ba..d1c08df5 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit c1b727bafc27dc715d7239a0bc63a1cdfe962bc2 +Subproject commit d1c08df5351ce8c3913723602a05268d593c9a45 From 4f5c332f4870eca116e14ccec8d833ca50fc9351 Mon Sep 17 00:00:00 2001 From: okay stopped screaming Date: Mon, 23 Feb 2026 23:10:57 +0200 Subject: [PATCH 4/4] use impl_default, remove some feature annotations --- app/.scratch.rs | 49 ++++ app/tek_impls.rs | 748 ++++++++++++++++++++--------------------------- tengri | 2 +- 3 files changed, 373 insertions(+), 426 deletions(-) diff --git a/app/.scratch.rs b/app/.scratch.rs index 1844b74f..6c98f399 100644 --- a/app/.scratch.rs +++ b/app/.scratch.rs @@ -1,4 +1,50 @@ + //pub fn track_counter (cache: &Arc>, track: usize, tracks: usize) + //-> Arc> + //{ + //let data = (track, tracks); + //cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1)); + //cache.read().unwrap().trks.view.clone() + //} + + //pub fn scene_add (cache: &Arc>, scene: usize, scenes: usize, is_editing: bool) + //-> impl Content + //{ + //let data = (scene, scenes); + //cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1)); + //button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing) + //} + + + + //pub fn view_h2 (&self) -> impl Content { + //let cache = self.project.clock.view_cache.clone(); + //let cache = cache.read().unwrap(); + //add(&Fixed::x(15, Align::w(Bsp::s( + //FieldH(theme, "Beat", cache.beat.view.clone()), + //FieldH(theme, "Time", cache.time.view.clone()), + //)))); + //add(&Fixed::x(13, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), + //)))); + //add(&Fixed::x(12, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), + //)))); + //add(&Bsp::s( + //Fill::X(Align::w(FieldH(theme, "Selected", Align::w(self.selection().describe( + //self.tracks(), + //self.scenes() + //))))), + //Fill::X(Align::w(FieldH(theme, format!("History ({})", self.history.len()), + //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))) + //)); + ////if let Some(last) = self.history.last() { + ////add(&FieldV(theme, format!("History ({})", self.history.len()), + ////Fill::X(Align::w(format!("{:?}", last.0))))); + ////} + //} /////////////////////////////////////////////////////////////////////////////////////////////////// //pub fn view_nil (_: &App) -> TuiCb { @@ -1137,3 +1183,6 @@ //cols //Thunk::new(|to: &mut TuiOut|{ //}) +//take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); + diff --git a/app/tek_impls.rs b/app/tek_impls.rs index 71cca19c..563ce8ff 100644 --- a/app/tek_impls.rs +++ b/app/tek_impls.rs @@ -16,20 +16,26 @@ impl Clip 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_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!(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); - has_clips!( |self: App|self.pool.clips); + 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()); + has_clips!( |self: App|self.pool.clips); impl_audio!(App: tek_jack_process, tek_jack_event); - impl_as_ref!(Vec: |self: App| self.project.as_ref()); - impl_as_mut!(Vec: |self: App| self.project.as_mut()); + 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) { @@ -45,12 +51,6 @@ mod app { } } - 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<'a> Namespace<'a, AppCommand> for App { symbols!('a |app| -> AppCommand { @@ -312,27 +312,18 @@ mod app { 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); - #[cfg(feature = "port")] impl_has!(Vec: |self: Arrangement| self.midi_ins); - #[cfg(feature = "port")] impl_has!(Vec: |self: Arrangement| self.midi_outs); - #[cfg(feature = "clock")] impl_has!(Clock: |self: Arrangement| self.clock); - #[cfg(feature = "select")] impl_has!(Selection: |self: Arrangement| self.selection); - #[cfg(feature = "select")] impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); - #[cfg(feature = "select")] impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); - #[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); - #[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); - - #[cfg(feature = "select")] impl Arrangement { - #[cfg(feature = "port")] fn selected_midi_in (&self) -> Option { todo!() } - #[cfg(feature = "port")] fn selected_midi_out (&self) -> Option { todo!() } - fn selected_device (&self) -> Option { todo!() } - fn unselect (&self) -> Selection { Selection::Nothing } - } - + 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 ( @@ -551,8 +542,7 @@ mod arrange { self.selected_track_mut()?.sampler_mut(0) } } - - #[cfg(feature = "scene")] impl ScenesView for Arrangement { + impl ScenesView for Arrangement { fn h_scenes (&self) -> u16 { (self.measure_height() as u16).saturating_sub(20) } @@ -563,7 +553,6 @@ mod arrange { (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 } } @@ -571,21 +560,7 @@ mod arrange { mod browse { use crate::*; - - 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) - } - } - } - 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![]; @@ -603,19 +578,10 @@ mod browse { } Ok(Self { cwd, dirs, files, ..Default::default() }) } - - 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 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 @@ -625,22 +591,10 @@ mod browse { unreachable!() }) } - - pub fn chdir (&self) -> Usually { - Self::new(Some(self.path())) - } - - fn _todo_stub_path_buf (&self) -> PathBuf { - todo!() - } - fn _todo_stub_usize (&self) -> usize { - todo!() - } - fn _todo_stub_arc_str (&self) -> Arc { - todo!() - } + fn _todo_stub_path_buf (&self) -> PathBuf { todo!() } + 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 { @@ -651,7 +605,6 @@ mod browse { }, |entry, _index|Fill::X(Align::w(entry))) } } - impl<'a> Iterator for EntriesIterator<'a> { type Item = Modify<&'a str>; fn next (&mut self) -> Option { @@ -669,285 +622,261 @@ mod browse { } } } -} -//take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); - - -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 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) + } + } } } -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 +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() } } - /// 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; - }, - _ => {} + 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() }; - } - - 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 track_counter (cache: &Arc>, track: usize, tracks: usize) - //-> Arc> - //{ - //let data = (track, tracks); - //cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1)); - //cache.read().unwrap().trks.view.clone() - //} - - //pub fn scene_add (cache: &Arc>, scene: usize, scenes: usize, is_editing: bool) - //-> impl Content - //{ - //let data = (scene, scenes); - //cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1)); - //button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing) - //} - - 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(bpm) = bpm { + clock.timebase.bpm.set(bpm); } - }); - 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)); + 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) } } - - //pub fn view_h2 (&self) -> impl Content { - //let cache = self.project.clock.view_cache.clone(); - //let cache = cache.read().unwrap(); - //add(&Fixed::x(15, Align::w(Bsp::s( - //FieldH(theme, "Beat", cache.beat.view.clone()), - //FieldH(theme, "Time", cache.time.view.clone()), - //)))); - //add(&Fixed::x(13, Align::w(Bsp::s( - //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), - //)))); - //add(&Fixed::x(12, Align::w(Bsp::s( - //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), - //)))); - //add(&Bsp::s( - //Fill::X(Align::w(FieldH(theme, "Selected", Align::w(self.selection().describe( - //self.tracks(), - //self.scenes() - //))))), - //Fill::X(Align::w(FieldH(theme, format!("History ({})", self.history.len()), - //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))) - //)); - ////if let Some(last) = self.history.last() { - ////add(&FieldV(theme, format!("History ({})", self.history.len()), - ////Fill::X(Align::w(format!("{:?}", last.0))))); - ////} - //} + 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 Connect { @@ -1114,55 +1043,10 @@ impl Default for Binding { } } } -impl Default for AppCommand { fn default () -> Self { Self::Nop } } -impl Default for MenuItem { fn default () -> Self { Self("".into(), Arc::new(Box::new(|_|Ok(())))) } } -impl Default for Timebase { fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) } } -impl Default for MidiEditor { fn default () -> Self { Self { size: Measure::new(0, 0), mode: PianoHorizontal::new(None) } } } -impl Default for OctaveVertical { - fn default () -> Self { - Self { on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] } - } -} -impl Default for MidiCursor { - fn default () -> Self { - Self { - time_pos: Arc::new(0.into()), - note_pos: Arc::new(36.into()), - note_len: Arc::new(24.into()), - } - } -} -impl Default for ClockView { - fn default () -> Self { - 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 Default for Pool { - fn default () -> Self { - //use PoolMode::*; - Self { - clip: 0.into(), - mode: None, - visible: true, - #[cfg(feature = "clip")] clips: Arc::from(RwLock::from(vec![])), - #[cfg(feature = "sampler")] samples: Arc::from(RwLock::from(vec![])), - #[cfg(feature = "browse")] browse: None, - } - } -} + +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) } } @@ -1549,6 +1433,11 @@ mod midi { 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 { @@ -2103,32 +1992,27 @@ mod audio { #[cfg(feature = "sequencer")] mod sequencer { use crate::*; - impl_has!(Sequencer: |self: Track| self.sequencer); - #[cfg(feature = "clock")] impl_has!(Clock: |self: Sequencer|self.clock); - #[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_ins); - #[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_outs); + 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 for Sequencer { - fn default () -> Self { - Self { - #[cfg(feature = "clock")] clock: Clock::default(), - #[cfg(feature = "clip")] play_clip: None, - #[cfg(feature = "clip")] next_clip: None, - #[cfg(feature = "port")] midi_ins: vec![], - #[cfg(feature = "port")] 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_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, @@ -2355,6 +2239,12 @@ mod audio { 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) { @@ -3494,6 +3384,14 @@ mod pool { 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) diff --git a/tengri b/tengri index d1c08df5..f1dda6af 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit d1c08df5351ce8c3913723602a05268d593c9a45 +Subproject commit f1dda6af07b94928481d062c3d3fda5b9e969633