From 35197fb826ad7f00b7fbd2494d335dd6fc59f13d Mon Sep 17 00:00:00 2001 From: okay stopped screaming Date: Sat, 21 Mar 2026 23:12:30 +0200 Subject: [PATCH] wip: nermalize --- src/arrange.rs | 586 +++++++- src/browse.rs | 254 ++++ src/clock.rs | 702 +++++++++ src/connect.rs | 306 ---- src/device.rs | 583 ++++++++ src/mix.rs | 56 + src/plugin.rs | 150 ++ src/sample.rs | 549 +++++++ src/sequence.rs | 820 +++++++++++ src/tek.rs | 1607 +++++++++++++-------- src/tek_impls.rs | 3586 ---------------------------------------------- src/tick.rs | 168 --- 12 files changed, 4649 insertions(+), 4718 deletions(-) create mode 100644 src/clock.rs delete mode 100644 src/connect.rs delete mode 100644 src/tek_impls.rs delete mode 100644 src/tick.rs diff --git a/src/arrange.rs b/src/arrange.rs index 0034912d..45417748 100644 --- a/src/arrange.rs +++ b/src/arrange.rs @@ -1,44 +1,7 @@ - -/// Represents the current user selection in the arranger -#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { - #[default] - /// Nothing is selected - Nothing, - /// The whole mix is selected - Mix, - /// A MIDI input is selected. - Input(usize), - /// A MIDI output is selected. - Output(usize), - /// A scene is selected. - #[cfg(feature = "scene")] Scene(usize), - /// A track is selected. - #[cfg(feature = "track")] Track(usize), - /// A clip (track × scene) is selected. - #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, - /// A track's MIDI input connection is selected. - #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, - /// A track's MIDI output connection is selected. - #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, - /// A track device slot is selected. - #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, -} - -/// A scene consists of a set of clips to play together. -/// -/// ``` -/// let scene: tek::Scene = Default::default(); -/// let _ = scene.pulses(); -/// let _ = scene.is_playing(&[]); -/// ``` -#[derive(Debug, Default)] pub struct Scene { - /// Name of scene - pub name: Arc, - /// Identifying color of scene - pub color: ItemTheme, - /// Clips in scene, one per track - pub clips: Vec>>>, -} +use ::std::sync::{Arc, RwLock}; +use ::tengri::{space::east, color::ItemTheme}; +use ::tengri::{draw::*, term::*}; +use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput}; /// Arranger. /// @@ -155,6 +118,47 @@ pub trait ClipsView: TracksView + ScenesView { } +/// Represents the current user selection in the arranger +#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { + #[default] + /// Nothing is selected + Nothing, + /// The whole mix is selected + Mix, + /// A MIDI input is selected. + Input(usize), + /// A MIDI output is selected. + Output(usize), + /// A scene is selected. + #[cfg(feature = "scene")] Scene(usize), + /// A track is selected. + #[cfg(feature = "track")] Track(usize), + /// A clip (track × scene) is selected. + #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, + /// A track's MIDI input connection is selected. + #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, + /// A track's MIDI output connection is selected. + #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, + /// A track device slot is selected. + #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, +} + +/// A scene consists of a set of clips to play together. +/// +/// ``` +/// let scene: tek::Scene = Default::default(); +/// let _ = scene.pulses(); +/// let _ = scene.is_playing(&[]); +/// ``` +#[derive(Debug, Default)] pub struct Scene { + /// Name of scene + pub name: Arc, + /// Identifying color of scene + pub color: ItemTheme, + /// Clips in scene, one per track + pub clips: Vec>>>, +} + pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured { fn tracks_width_available (&self) -> u16 { @@ -414,3 +418,503 @@ pub trait HasTrack: AsRefOpt + AsMutOpt { self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) } } + impl_as_ref!(Vec: |self: App| self.project.as_ref()); + impl_as_mut!(Vec: |self: App| self.project.as_mut()); + #[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt()); + #[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt()); + impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } + impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } } + + impl HasWidth for Track { + const MIN_WIDTH: usize = 9; + fn width_inc (&mut self) { self.width += 1; } + fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } + } + + impl Track { + /// Create a new track with only the default [Sequencer]. + pub fn new ( + name: &impl AsRef, + color: Option, + jack: &Jack<'static>, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[Connect], + midi_to: &[Connect], + ) -> Usually { + Ok(Self { + name: name.as_ref().into(), + color: color.unwrap_or_default(), + sequencer: Sequencer::new(format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to)?, + ..Default::default() + }) + } + pub fn audio_ins (&self) -> &[AudioInput] { + self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() + } + pub fn audio_outs (&self) -> &[AudioOutput] { + self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() + } + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + pub fn per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Draw + 'a { + iter(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + let width = (x2 - x1) as u16; + iter_east(x1 as u16, width, w_exact(width, Tui::fg_bg( + track.color.lightest.rgb, + track.color.base.rgb, + callback(index, track))))}) + } + /// Create a new track connecting the [Sequencer] to a [Sampler]. + #[cfg(feature = "sampler")] pub fn new_with_sampler ( + name: &impl AsRef, + color: Option, + jack: &Jack<'static>, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[Connect], + midi_to: &[Connect], + audio_from: &[&[Connect];2], + audio_to: &[&[Connect];2], + ) -> Usually { + let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; + let client_name = jack.with_client(|c|c.name().to_string()); + let port_name = track.sequencer.midi_outs[0].port_name(); + let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; + track.devices.push(Device::Sampler(Sampler::new( + jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to + )?)); + Ok(track) + } + #[cfg(feature = "sampler")] pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { + for device in self.devices.iter() { + match device { + Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, + _ => {} + } + } + None + } + #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { + for device in self.devices.iter_mut() { + match device { + Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, + _ => {} + } + } + None + } + } + + pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Draw + 'a { + per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track)))) + } + + pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Draw + 'a { + origin_x(Tui::bg(Reset, iter(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + let width = (x2 - x1) as u16; + iter_east(x1 as u16, width, w_exact(width, Tui::fg_bg( + track.color.lightest.rgb, + track.color.base.rgb, + callback(index, track))))}))) + } + #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt()); + #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); + #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene()); + #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut()); + impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } + impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } + impl ScenesView for App { + fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(self.w_side()) } + fn w_side (&self) -> u16 { 20 } + fn h_scenes (&self) -> u16 { (self.measure_height() as u16).saturating_sub(20) } + } + impl Scene { + /// Returns the pulse length of the longest clip in the scene + pub fn pulses (&self) -> usize { + self.clips.iter().fold(0, |a, p|{ + a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) + }) + } + /// Returns true if all clips in the scene are + /// currently playing on the given collection of tracks. + pub fn is_playing (&self, tracks: &[Track]) -> bool { + self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() + .all(|(track_index, clip)|match clip { + Some(c) => tracks + .get(track_index) + .map(|track|{ + if let Some((_, Some(clip))) = track.sequencer().play_clip() { + *clip.read().unwrap() == *c.read().unwrap() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) + } + pub fn clip (&self, index: usize) -> Option<&Arc>> { + match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } + } + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + } + +impl Selection { + pub fn describe ( + &self, + #[cfg(feature = "track")] tracks: &[Track], + #[cfg(feature = "scene")] scenes: &[Scene], + ) -> Arc { + use Selection::*; + format!("{}", match self { + Mix => "Everything".to_string(), + #[cfg(feature = "scene")] Scene(s) => + scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), + #[cfg(feature = "track")] Track(t) => + tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), + TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { + (Some(_), Some(s)) => match s.clip(*track) { + Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), + None => format!("T{track} S{scene}: Empty") + }, + _ => format!("T{track} S{scene}: Empty"), + }, + _ => todo!() + }).into() + } + #[cfg(feature = "scene")] pub fn scene (&self) -> Option { + use Selection::*; + match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } + } + #[cfg(feature = "scene")] pub fn select_scene (&self, scene_count: usize) -> Self { + use Selection::*; + match self { + Mix | Track(_) => Scene(0), + Scene(s) => Scene((s + 1) % scene_count), + TrackClip { scene, .. } => Track(*scene), + _ => todo!(), + } + } + #[cfg(feature = "scene")] pub fn select_scene_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Scene(0), + Track(t) => TrackClip { track: *t, scene: 0 }, + Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, + TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, + _ => todo!() + } + } + #[cfg(feature = "scene")] pub fn select_scene_prev (&self) -> Self { + use Selection::*; + match self { + Mix | Scene(0) => Mix, + Scene(s) => Scene(s - 1), + Track(t) => Track(*t), + TrackClip { track, scene: 0 } => Track(*track), + TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, + _ => todo!() + } + } + #[cfg(feature = "track")] pub fn track (&self) -> Option { + use Selection::*; + if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { + Some(*track) + } else { + None + } + } + #[cfg(feature = "track")] pub fn select_track (&self, track_count: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(_) => Mix, + Track(t) => Track((t + 1) % track_count), + TrackClip { track, .. } => Track(*track), + _ => todo!(), + } + } + #[cfg(feature = "track")] pub fn select_track_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(s) => TrackClip { track: 0, scene: *s }, + Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, + TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, + _ => todo!() + } + } + #[cfg(feature = "track")] pub fn select_track_prev (&self) -> Self { + use Selection::*; + match self { + Mix => Mix, + Scene(s) => Scene(*s), + Track(0) => Mix, + Track(t) => Track(t - 1), + TrackClip { track: 0, scene } => Scene(*scene), + TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, + _ => todo!() + } + } +} + impl_has!(Jack<'static>: |self: Arrangement| self.jack); + impl_has!(Measure: |self: Arrangement| self.size); + impl_has!(Vec: |self: Arrangement| self.tracks); + impl_has!(Vec: |self: Arrangement| self.scenes); + impl_has!(Vec: |self: Arrangement| self.midi_ins); + impl_has!(Vec: |self: Arrangement| self.midi_outs); + impl_has!(Clock: |self: Arrangement| self.clock); + impl_has!(Selection: |self: Arrangement| self.selection); + impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); + impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); + impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); + impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); + impl Arrangement { + /// Create a new arrangement. + pub fn new ( + jack: &Jack<'static>, + name: Option>, + clock: Clock, + tracks: Vec, + scenes: Vec, + midi_ins: Vec, + midi_outs: Vec, + ) -> Self { + Self { + clock, tracks, scenes, midi_ins, midi_outs, + jack: jack.clone(), + name: name.unwrap_or_default(), + color: ItemTheme::random(), + selection: Selection::TrackClip { track: 0, scene: 0 }, + ..Default::default() + } + } + /// Width of display + pub fn w (&self) -> u16 { + self.size.w() as u16 + } + /// Width allocated for sidebar. + pub fn w_sidebar (&self, is_editing: bool) -> u16 { + self.w() / if is_editing { 16 } else { 8 } as u16 + } + /// Width available to display tracks. + pub fn w_tracks_area (&self, is_editing: bool) -> u16 { + self.w().saturating_sub(self.w_sidebar(is_editing)) + } + /// Height of display + pub fn h (&self) -> u16 { + self.size.h() as u16 + } + /// Height taken by visible device slots. + pub fn h_devices (&self) -> u16 { + 2 + //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) + } + /// Add multiple tracks + #[cfg(feature = "track")] pub fn tracks_add ( + &mut self, + count: usize, width: Option, + mins: &[Connect], mouts: &[Connect], + ) -> Usually<()> { + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..count { + let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); + let track = self.track_add(None, Some(color), mins, mouts)?.1; + if let Some(width) = width { + track.width = width; + } + } + Ok(()) + } + /// Add a track + #[cfg(feature = "track")] pub fn track_add ( + &mut self, + name: Option<&str>, color: Option, + mins: &[Connect], mouts: &[Connect], + ) -> Usually<(usize, &mut Track)> { + let name: Arc = name.map_or_else( + ||format!("trk{:02}", self.track_last).into(), + |x|x.to_string().into() + ); + self.track_last += 1; + let track = Track { + width: (name.len() + 2).max(12), + color: color.unwrap_or_else(ItemTheme::random), + sequencer: Sequencer::new( + &format!("{name}"), + self.jack(), + Some(self.clock()), + None, + mins, + mouts + )?, + name, + ..Default::default() + }; + self.tracks_mut().push(track); + let len = self.tracks().len(); + let index = len - 1; + for scene in self.scenes_mut().iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); + } + } + Ok((index, &mut self.tracks_mut()[index])) + } + #[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { + south( + h_exact(1, self.view_inputs_header()), + Thunk::new(|to: &mut Tui|{ + for (index, port) in self.midi_ins().iter().enumerate() { + to.place(&x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port)))) + } + }) + ) + } + #[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Draw + '_ { + east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), + west(w_exact(4, button_2("I", "+", false)), Thunk::new(move|to: &mut Tui|for (_index, track, x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] + to.place(&x_push(x1 as u16, Tui::bg(track.color.dark.rgb, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), + either(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), + either(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), + )))))) + }))) + } + #[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Draw { + east(w_exact(20, origin_w(east(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), + west(w_exact(4, ()), Thunk::new(move|to: &mut Tui|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] + to.place(&Tui::bg(track.color.darker.rgb, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), + either(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), + either(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), + ))))) + }))) + } + #[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { + let mut h = 1; + for output in self.midi_outs().iter() { + h += 1 + output.connections.len(); + } + let h = h as u16; + let list = south( + h_exact(1, w_full(origin_w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), + h_exact(h - 1, wh_full(origin_nw(Thunk::new(|to: &mut Tui|{ + for (_index, port) in self.midi_outs().iter().enumerate() { + to.place(&h_exact(1,w_full(east( + origin_w(east(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), + w_full(origin_e(format!("{}/{} ", + port.port().get_connections().len(), + port.connections.len()))))))); + for (index, conn) in port.connections.iter().enumerate() { + to.place(&h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info()))))); + } + } + }))))); + h_exact(h, view_track_row_section(theme, list, button_2("O", "+", false), + Tui::bg(theme.darker.rgb, origin_w(w_full( + Thunk::new(|to: &mut Tui|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&w_exact(track_width(index, track), + Thunk::new(|to: &mut Tui|{ + to.place(&h_exact(1, origin_w(east( + either(true, Tui::fg(Green, "play "), "play "), + either(false, Tui::fg(Yellow, "solo "), "solo "), + )))); + for (_index, port) in self.midi_outs().iter().enumerate() { + to.place(&h_exact(1, origin_w(east( + either(true, Tui::fg(Green, " ● "), " · "), + either(false, Tui::fg(Yellow, " ● "), " · "), + )))); + for (_index, _conn) in port.connections.iter().enumerate() { + to.place(&h_exact(1, w_full(""))); + } + }})))}})))))) + } + #[cfg(feature = "track")] pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { + let mut h = 2u16; + for track in self.tracks().iter() { + h = h.max(track.devices.len() as u16 * 2); + } + view_track_row_section(theme, + button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), + button_2("D", "+", false), + Thunk::new(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&wh_exact(track_width(index, track), h + 1, + Tui::bg(track.color.dark.rgb, origin_nw(iter_south(2, move||0..h, + |_, _index|wh_exact(track.width as u16, 2, + Tui::fg_bg( + ItemTheme::G[32].lightest.rgb, + ItemTheme::G[32].dark.rgb, + origin_nw(format!(" · {}", "--"))))))))); + })) + } + /// Put a clip in a slot + #[cfg(feature = "clip")] pub fn clip_put ( + &mut self, track: usize, scene: usize, clip: Option>> + ) -> Option>> { + let old = self.scenes[scene].clips[track].clone(); + self.scenes[scene].clips[track] = clip; + old + } + /// Change the color of a clip, returning the previous one + #[cfg(feature = "clip")] pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) + -> Option + { + self.scenes[scene].clips[track].as_ref().map(|clip|{ + let mut clip = clip.write().unwrap(); + let old = clip.color.clone(); + clip.color = color.clone(); + panic!("{color:?} {old:?}"); + //old + }) + } + /// Toggle looping for the active clip + #[cfg(feature = "clip")] pub fn toggle_loop (&mut self) { + if let Some(clip) = self.selected_clip() { + clip.write().unwrap().toggle_loop() + } + } + /// Get the first sampler of the active track + #[cfg(feature = "sampler")] pub fn sampler (&self) -> Option<&Sampler> { + self.selected_track()?.sampler(0) + } + /// Get the first sampler of the active track + #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { + self.selected_track_mut()?.sampler_mut(0) + } + } + impl ScenesView for Arrangement { + fn h_scenes (&self) -> u16 { + (self.measure_height() as u16).saturating_sub(20) + } + fn w_side (&self) -> u16 { + (self.measure_width() as u16 * 2 / 10).max(20) + } + fn w_mid (&self) -> u16 { + (self.measure_width() as u16).saturating_sub(2 * self.w_side()).max(40) + } + } + impl HasClipsSize for Arrangement { + fn clips_size (&self) -> &Measure { &self.size_inner } + } diff --git a/src/browse.rs b/src/browse.rs index ccf7e37c..f9ca478b 100644 --- a/src/browse.rs +++ b/src/browse.rs @@ -1,3 +1,7 @@ +use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; +use crate::sequence::MidiClip; +use crate::sample::Sample; + /// Browses for files to load/save. /// /// ``` @@ -95,3 +99,253 @@ pub struct PoolView<'a>(pub &'a Pool); /// Editing the number of ticks Tick, } +has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); +impl_has_clips!(|self: Pool|self.clips); +impl_from!(Pool: |clip:&Arc>|{ + let model = Self::default(); + model.clips.write().unwrap().push(clip.clone()); + model.clip.store(1, Relaxed); + model +}); +impl_default!(Pool: Self { + browse: None, + clip: 0.into(), + clips: Arc::from(RwLock::from(vec![])), + mode: None, + samples: Arc::from(RwLock::from(vec![])), + visible: true, +}); +impl Pool { + pub fn clip_index (&self) -> usize { + self.clip.load(Relaxed) + } + pub fn set_clip_index (&self, value: usize) { + self.clip.store(value, Relaxed); + } + pub fn mode (&self) -> &Option { + &self.mode + } + pub fn mode_mut (&mut self) -> &mut Option { + &mut self.mode + } + pub fn begin_clip_length (&mut self) { + let length = self.clips()[self.clip_index()].read().unwrap().length; + *self.mode_mut() = Some(PoolMode::Length( + self.clip_index(), + length, + ClipLengthFocus::Bar + )); + } + pub fn begin_clip_rename (&mut self) { + let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); + *self.mode_mut() = Some(PoolMode::Rename( + self.clip_index(), + name + )); + } + pub fn begin_import (&mut self) -> Usually<()> { + *self.mode_mut() = Some(PoolMode::Import( + self.clip_index(), + Browse::new(None)? + )); + Ok(()) + } + pub fn begin_export (&mut self) -> Usually<()> { + *self.mode_mut() = Some(PoolMode::Export( + self.clip_index(), + Browse::new(None)? + )); + Ok(()) + } + pub fn new_clip (&self) -> MidiClip { + MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) + } + pub fn cloned_clip (&self) -> MidiClip { + let index = self.clip_index(); + let mut clip = self.clips()[index].read().unwrap().duplicate(); + clip.color = ItemTheme::random_near(clip.color, 0.25); + clip + } + pub fn add_new_clip (&self) -> (usize, Arc>) { + let clip = Arc::new(RwLock::new(self.new_clip())); + let index = { + let mut clips = self.clips.write().unwrap(); + clips.push(clip.clone()); + clips.len().saturating_sub(1) + }; + self.clip.store(index, Relaxed); + (index, clip) + } + pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { + let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); + if let Some(index) = index { + self.clips.write().unwrap().remove(index); + return true + } + false + } +} +impl ClipLengthFocus { + pub fn next (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } + } + pub fn prev (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } + } +} +impl ClipLength { + pub fn _new (pulses: usize, focus: Option) -> Self { + Self { ppq: PPQ, bpb: 4, pulses, focus } + } + pub fn bars (&self) -> usize { + self.pulses / (self.bpb * self.ppq) + } + pub fn beats (&self) -> usize { + (self.pulses % (self.bpb * self.ppq)) / self.ppq + } + pub fn ticks (&self) -> usize { + self.pulses % self.ppq + } + pub fn bars_string (&self) -> Arc { + format!("{}", self.bars()).into() + } + pub fn beats_string (&self) -> Arc { + format!("{}", self.beats()).into() + } + pub fn ticks_string (&self) -> Arc { + format!("{:>02}", self.ticks()).into() + } +} + +impl Pool { + fn _todo_usize_ (&self) -> usize { todo!() } + fn _todo_bool_ (&self) -> bool { todo!() } + fn _todo_clip_ (&self) -> MidiClip { todo!() } + fn _todo_path_ (&self) -> PathBuf { todo!() } + fn _todo_color_ (&self) -> ItemColor { todo!() } + fn _todo_str_ (&self) -> Arc { todo!() } + fn _clip_new (&self) -> MidiClip { self.new_clip() } + fn _clip_cloned (&self) -> MidiClip { self.cloned_clip() } + fn _clip_index_current (&self) -> usize { 0 } + fn _clip_index_after (&self) -> usize { 0 } + fn _clip_index_previous (&self) -> usize { 0 } + fn _clip_index_next (&self) -> usize { 0 } + fn _color_random (&self) -> ItemColor { ItemColor::random() } +} + +impl<'a> PoolView<'a> { + fn tui (&self) -> impl Draw { + let Self(pool) = self; + //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); + //let on_bg = |x|x;//below(Repeat(" "), Tui::bg(color.darkest.rgb, x)); + //let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); + //let height = pool.clips.read().unwrap().len() as u16; + w_exact(20, h_full(origin_n(iter( + ||pool.clips().clone().into_iter(), + move|clip: Arc>, i: usize|{ + let item_height = 1; + let item_offset = i as u16 * item_height; + let selected = i == pool.clip_index(); + let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); + let bg = if selected { color.light.rgb } else { color.base.rgb }; + let fg = color.lightest.rgb; + let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; + let length = if false { String::default() } else { format!("{length} ") }; + h_exact(1, iter_south(item_offset, item_height, Tui::bg(bg, below!( + w_full(origin_w(Tui::fg(fg, Tui::bold(selected, name)))), + w_full(origin_e(Tui::fg(fg, Tui::bold(selected, length)))), + w_full(origin_w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), + w_full(origin_e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), + )))) + })))) + } +} +impl ClipLength { + fn tui (&self) -> impl Draw { + use ClipLengthFocus::*; + let bars = ||self.bars_string(); + let beats = ||self.beats_string(); + let ticks = ||self.ticks_string(); + match self.focus { + None => east!(" ", bars(), ".", beats(), ".", ticks()), + Some(Bar) => east!("[", bars(), "]", beats(), ".", ticks()), + Some(Beat) => east!(" ", bars(), "[", beats(), "]", ticks()), + Some(Tick) => east!(" ", bars(), ".", beats(), "[", ticks()), + } + } +} + impl Browse { + pub fn new (cwd: Option) -> Usually { + let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; + let mut dirs = vec![]; + let mut files = vec![]; + for entry in std::fs::read_dir(&cwd)? { + let entry = entry?; + let name = entry.file_name(); + let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); + let meta = entry.metadata()?; + if meta.is_dir() { + dirs.push((name, format!("📁 {decoded}"))); + } else if meta.is_file() { + files.push((name, format!("📄 {decoded}"))); + } + } + Ok(Self { cwd, dirs, files, ..Default::default() }) + } + pub fn chdir (&self) -> Usually { Self::new(Some(self.path())) } + pub fn len (&self) -> usize { self.dirs.len() + self.files.len() } + pub fn is_dir (&self) -> bool { self.index < self.dirs.len() } + pub fn is_file (&self) -> bool { self.index >= self.dirs.len() } + pub fn path (&self) -> PathBuf { + self.cwd.join(if self.is_dir() { + &self.dirs[self.index].0 + } else if self.is_file() { + &self.files[self.index - self.dirs.len()].0 + } else { + unreachable!() + }) + } + fn _todo_stub_path_buf (&self) -> PathBuf { todo!() } + fn _todo_stub_usize (&self) -> usize { todo!() } + fn _todo_stub_arc_str (&self) -> Arc { todo!() } + } + impl Browse { + fn tui (&self) -> impl Draw { + iter_south(1, ||EntriesIterator { + offset: 0, + index: 0, + length: self.dirs.len() + self.files.len(), + browser: self, + }, |entry, _index|w_full(origin_w(entry))) + } + } + impl<'a> Iterator for EntriesIterator<'a> { + type Item = Modify<&'a str>; + fn next (&mut self) -> Option { + let dirs = self.browser.dirs.len(); + let files = self.browser.files.len(); + let index = self.index; + if self.index < dirs { + self.index += 1; + Some(Tui::bold(true, self.browser.dirs[index].1.as_str())) + } else if self.index < dirs + files { + self.index += 1; + Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str())) + } else { + None + } + } + } + impl PartialEq for BrowseTarget { + fn eq (&self, other: &Self) -> bool { + match self { + Self::ImportSample(_) => false, + Self::ExportSample(_) => false, + Self::ImportClip(_) => false, + Self::ExportClip(_) => false, + #[allow(unused)] t => matches!(other, t) + } + } + } diff --git a/src/clock.rs b/src/clock.rs new file mode 100644 index 00000000..2d78af28 --- /dev/null +++ b/src/clock.rs @@ -0,0 +1,702 @@ +use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; +use ::atomic_float::AtomicF64; +use ::tengri::{draw::*, term::*}; + +pub trait HasClock: AsRef + AsMut { + fn clock (&self) -> &Clock { self.as_ref() } + fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } +} + +/// The source of time. +/// +/// ``` +/// let clock = tek::Clock::default(); +/// ``` +#[derive(Clone, Default)] pub struct Clock { + /// JACK transport handle. + pub transport: Arc>, + /// Global temporal resolution (shared by [Moment] fields) + pub timebase: Arc, + /// Current global sample and usec (monotonic from JACK clock) + pub global: Arc, + /// Global sample and usec at which playback started + pub started: Arc>>, + /// Playback offset (when playing not from start) + pub offset: Arc, + /// Current playhead position + pub playhead: Arc, + /// Note quantization factor + pub quant: Arc, + /// Launch quantization factor + pub sync: Arc, + /// Size of buffer in samples + pub chunk: Arc, + // Cache of formatted strings + pub view_cache: Arc>, + /// For syncing the clock to an external source + #[cfg(feature = "port")] pub midi_in: Arc>>, + /// For syncing other devices to this clock + #[cfg(feature = "port")] pub midi_out: Arc>>, + /// For emitting a metronome + #[cfg(feature = "port")] pub click_out: Arc>>, +} + +/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) +/// +/// ``` +/// let _ = tek::Timebase::default(); +/// ``` +#[derive(Debug, Clone)] pub struct Timebase { + /// Audio samples per second + pub sr: SampleRate, + /// MIDI beats per minute + pub bpm: Bpm, + /// MIDI ticks per beat + pub ppq: Ppq, +} + +/// Iterator that emits subsequent ticks within a range. +/// +/// ``` +/// let iter = tek::Ticker::default(); +/// ``` +#[derive(Debug, Default)] pub struct Ticker { + pub spp: f64, + pub sample: usize, + pub start: usize, + pub end: usize, +} + +/// A point in time in all time scales (microsecond, sample, MIDI pulse) +/// +/// ``` +/// let _ = tek::Moment::default(); +/// ``` +#[derive(Debug, Default, Clone)] pub struct Moment { + pub timebase: Arc, + /// Current time in microseconds + pub usec: Microsecond, + /// Current time in audio samples + pub sample: SampleCount, + /// Current time in MIDI pulses + pub pulse: Pulse, +} + +/// +/// ``` +/// let _ = tek::Moment2::default(); +/// ``` +#[derive(Debug, Clone, Default)] pub enum Moment2 { + #[default] None, + Zero, + Usec(Microsecond), + Sample(SampleCount), + Pulse(Pulse), +} + +/// MIDI resolution in PPQ (pulses per quarter note) +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64); + +/// Timestamp in MIDI pulses +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64); + +/// Tempo in beats per minute +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64); + +/// Quantization setting for launching clips +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64); + +/// Quantization setting for notes +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64); + +/// Timestamp in audio samples +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64); + +/// Audio sample rate in Hz (samples per second) +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64); + +/// Timestamp in microseconds +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64); + +/// A unit of time, represented as an atomic 64-bit float. +/// +/// According to https://stackoverflow.com/a/873367, as per IEEE754, +/// every integer between 1 and 2^53 can be represented exactly. +/// This should mean that, even at 192kHz sampling rate, over 1 year of audio +/// can be clocked in microseconds with f64 without losing precision. +pub trait TimeUnit: InteriorMutable {} + +/// Contains memoized renders of clock values. +/// +/// Performance optimization. +#[derive(Debug)] pub struct ClockView { + pub sr: Memo, String>, + pub buf: Memo, String>, + pub lat: Memo, String>, + pub bpm: Memo, String>, + pub beat: Memo, String>, + pub time: Memo, String>, +} + +/// FIXME: remove this and use PPQ from timebase everywhere: +pub const PPQ: usize = 96; + +/// (pulses, name), assuming 96 PPQ +pub const NOTE_DURATIONS: [(usize, &str);26] = [ + (1, "1/384"), (2, "1/192"), + (3, "1/128"), (4, "1/96"), + (6, "1/64"), (8, "1/48"), + (12, "1/32"), (16, "1/24"), + (24, "1/16"), (32, "1/12"), + (48, "1/8"), (64, "1/6"), + (96, "1/4"), (128, "1/3"), + (192, "1/2"), (256, "2/3"), + (384, "1/1"), (512, "4/3"), + (576, "3/2"), (768, "2/1"), + (1152, "3/1"), (1536, "4/1"), + (2304, "6/1"), (3072, "8/1"), + (3456, "9/1"), (6144, "16/1"), +]; + +pub const NOTE_NAMES: [&str; 128] = [ + "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", + "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", + "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", + "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", + "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", + "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", + "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6", + "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", + "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", + "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", + "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", +]; + +pub const DEFAULT_PPQ: f64 = 96.0; + +def_command!(ClockCommand: |clock: Clock| { + SeekUsec { usec: f64 } => { + clock.playhead.update_from_usec(*usec); Ok(None) }, + SeekSample { sample: f64 } => { + clock.playhead.update_from_sample(*sample); Ok(None) }, + SeekPulse { pulse: f64 } => { + clock.playhead.update_from_pulse(*pulse); Ok(None) }, + SetBpm { bpm: f64 } => Ok(Some( + Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })), + SetQuant { quant: f64 } => Ok(Some( + Self::SetQuant { quant: clock.quant.set(*quant) })), + SetSync { sync: f64 } => Ok(Some( + Self::SetSync { sync: clock.sync.set(*sync) })), + + Play { position: Option } => { + clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ }, + Pause { position: Option } => { + clock.pause_at(*position)?; Ok(None) }, + + TogglePlayback { position: u32 } => Ok(if clock.is_rolling() { + clock.pause_at(Some(*position))?; None + } else { + clock.play_from(Some(*position))?; None + }), +}); + +impl Moment { + pub fn zero (timebase: &Arc) -> Self { + Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } + } + pub fn from_usec (timebase: &Arc, usec: f64) -> Self { + Self { + usec: usec.into(), + sample: timebase.sr.usecs_to_sample(usec).into(), + pulse: timebase.usecs_to_pulse(usec).into(), + timebase: timebase.clone(), + } + } + pub fn from_sample (timebase: &Arc, sample: f64) -> Self { + Self { + sample: sample.into(), + usec: timebase.sr.samples_to_usec(sample).into(), + pulse: timebase.samples_to_pulse(sample).into(), + timebase: timebase.clone(), + } + } + pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { + Self { + pulse: pulse.into(), + sample: timebase.pulses_to_sample(pulse).into(), + usec: timebase.pulses_to_usec(pulse).into(), + timebase: timebase.clone(), + } + } + #[inline] pub fn update_from_usec (&self, usec: f64) { + self.usec.set(usec); + self.pulse.set(self.timebase.usecs_to_pulse(usec)); + self.sample.set(self.timebase.sr.usecs_to_sample(usec)); + } + #[inline] pub fn update_from_sample (&self, sample: f64) { + self.usec.set(self.timebase.sr.samples_to_usec(sample)); + self.pulse.set(self.timebase.samples_to_pulse(sample)); + self.sample.set(sample); + } + #[inline] pub fn update_from_pulse (&self, pulse: f64) { + self.usec.set(self.timebase.pulses_to_usec(pulse)); + self.pulse.set(pulse); + self.sample.set(self.timebase.pulses_to_sample(pulse)); + } + #[inline] pub fn format_beat (&self) -> Arc { + self.timebase.format_beats_1(self.pulse.get()).into() + } +} +impl LaunchSync { + pub fn next (&self) -> f64 { + note_duration_next(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + note_duration_prev(self.get() as usize) as f64 + } +} +impl Quantize { + pub fn next (&self) -> f64 { + note_duration_next(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + note_duration_prev(self.get() as usize) as f64 + } +} +impl Timebase { + /// Specify sample rate, BPM and PPQ + pub fn new ( + s: impl Into, + b: impl Into, + p: impl Into + ) -> Self { + Self { sr: s.into(), bpm: b.into(), ppq: p.into() } + } + /// Iterate over ticks between start and end. + #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> Ticker { + Ticker { spp: self.samples_per_pulse(), sample: start, start, end } + } + /// Return the duration fo a beat in microseconds + #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } + /// Return the number of beats in a second + #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } + /// Return the number of microseconds corresponding to a note of the given duration + #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { + 4.0 * self.usec_per_beat() * num / den + } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } + /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) + #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } + /// Return number of pulses in a second (BPM-dependent) + #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } + /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) + #[inline] pub fn pulses_per_sample (&self) -> f64 { + self.usec_per_pulse() / self.sr.usec_per_sample() + } + /// Return number of samples in a pulse (SR- and BPM-dependent) + #[inline] pub fn samples_per_pulse (&self) -> f64 { + self.sr.get() / self.pulses_per_second() + } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { + self.pulses_per_sample() * p + } + /// Convert a number of samples to a pulse number (SR- and BPM-dependent) + #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { + s / self.pulses_per_sample() + } + /// Return the number of samples corresponding to a note of the given duration + #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { + self.usec_to_sample(self.note_to_usec(note)) + } + /// Return the number of samples corresponding to the given number of microseconds + #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { + usec * self.sr.get() / 1000f64 + } + /// Return the quantized position of a moment in time given a step + #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { + let step = self.note_to_usec(step); + (time / step, time % step) + } + /// Quantize a collection of events + #[inline] pub fn quantize_into + Sized, T> ( + &self, step: (f64, f64), events: E + ) -> Vec<(f64, f64)> { + events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 0 + #[inline] pub fn format_beats_0 (&self, pulse: f64) -> Arc { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + format!("{}.{}.{pulses:02}", beats / 4, beats % 4).into() + } + /// Format a number of pulses into Beat.Bar starting from 0 + #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4, beats % 4).into() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc { + let mut string = String::with_capacity(16); + self.format_beats_1_to(&mut string, pulse).expect("failed to format {pulse} into beat"); + string.into() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1_to (&self, w: &mut impl std::fmt::Write, pulse: f64) -> Result<(), std::fmt::Error> { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + write!(w, "{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4 + 1, beats % 4 + 1).into() + } +} +impl SampleRate { + /// Return the duration of a sample in microseconds (floating) + #[inline] pub fn usec_per_sample (&self) -> f64 { + 1_000_000f64 / self.get() + } + /// Return the duration of a sample in microseconds (floating) + #[inline] pub fn sample_per_usec (&self) -> f64 { + self.get() / 1_000_000f64 + } + /// Convert a number of samples to microseconds (floating) + #[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 { + self.usec_per_sample() * samples + } + /// Convert a number of microseconds to samples (floating) + #[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 { + self.sample_per_usec() * usecs + } +} +impl Microsecond { + #[inline] pub fn format_msu (&self) -> Arc { + let usecs = self.get() as usize; + let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); + let (minutes, seconds) = (seconds / 60, seconds % 60); + format!("{minutes}:{seconds:02}:{msecs:03}").into() + } +} + +/// Define and implement a unit of time +#[macro_export] macro_rules! impl_time_unit { +($T:ident) => { + impl Gettable for $T { + fn get (&self) -> f64 { self.0.load(Relaxed) } + } + impl InteriorMutable for $T { + fn set (&self, value: f64) -> f64 { + let old = self.get(); + self.0.store(value, Relaxed); + old + } + } + impl TimeUnit for $T {} + impl_op!($T, Add, add, |a, b|{a + b}); + impl_op!($T, Sub, sub, |a, b|{a - b}); + impl_op!($T, Mul, mul, |a, b|{a * b}); + impl_op!($T, Div, div, |a, b|{a / b}); + impl_op!($T, Rem, rem, |a, b|{a % b}); + impl From for $T { fn from (value: f64) -> Self { Self(value.into()) } } + impl From for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } } + impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } } + impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } } + impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } } + impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } } + impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } +} +} + +impl_time_unit!(SampleCount); +impl_time_unit!(SampleRate); +impl_time_unit!(Microsecond); +impl_time_unit!(Quantize); +impl_time_unit!(Ppq); +impl_time_unit!(Pulse); +impl_time_unit!(Bpm); +impl_time_unit!(LaunchSync); + impl std::fmt::Debug for Clock { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Clock") + .field("timebase", &self.timebase) + .field("chunk", &self.chunk) + .field("quant", &self.quant) + .field("sync", &self.sync) + .field("global", &self.global) + .field("playhead", &self.playhead) + .field("started", &self.started) + .finish() + } + } + impl Clock { + pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { + let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); + let timebase = Arc::new(Timebase::default()); + let clock = Self { + quant: Arc::new(24.into()), + sync: Arc::new(384.into()), + transport: Arc::new(Some(transport)), + chunk: Arc::new((chunk as usize).into()), + global: Arc::new(Moment::zero(&timebase)), + playhead: Arc::new(Moment::zero(&timebase)), + offset: Arc::new(Moment::zero(&timebase)), + started: RwLock::new(None).into(), + timebase, + midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), + midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), + click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), + ..Default::default() + }; + if let Some(bpm) = bpm { + clock.timebase.bpm.set(bpm); + } + Ok(clock) + } + pub fn timebase (&self) -> &Arc { + &self.timebase + } + /// Current sample rate + pub fn sr (&self) -> &SampleRate { + &self.timebase.sr + } + /// Current tempo + pub fn bpm (&self) -> &Bpm { + &self.timebase.bpm + } + /// Current MIDI resolution + pub fn ppq (&self) -> &Ppq { + &self.timebase.ppq + } + /// Next pulse that matches launch sync (for phrase switchover) + pub fn next_launch_pulse (&self) -> usize { + let sync = self.sync.get() as usize; + let pulse = self.playhead.pulse.get() as usize; + if pulse % sync == 0 { + pulse + } else { + (pulse / sync + 1) * sync + } + } + /// Start playing, optionally seeking to a given location beforehand + pub fn play_from (&self, start: Option) -> Usually<()> { + if let Some(transport) = self.transport.as_ref() { + if let Some(start) = start { + transport.locate(start)?; + } + transport.start()?; + } + Ok(()) + } + /// Pause, optionally seeking to a given location afterwards + pub fn pause_at (&self, pause: Option) -> Usually<()> { + if let Some(transport) = self.transport.as_ref() { + transport.stop()?; + if let Some(pause) = pause { + transport.locate(pause)?; + } + } + Ok(()) + } + /// Is currently paused? + pub fn is_stopped (&self) -> bool { + self.started.read().unwrap().is_none() + } + /// Is currently playing? + pub fn is_rolling (&self) -> bool { + self.started.read().unwrap().is_some() + } + /// Update chunk size + pub fn set_chunk (&self, n_frames: usize) { + self.chunk.store(n_frames, Relaxed); + } + pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { + // Store buffer length + self.set_chunk(scope.n_frames() as usize); + + // Store reported global frame and usec + let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; + self.global.sample.set(current_frames as f64); + self.global.usec.set(current_usecs as f64); + + let mut started = self.started.write().unwrap(); + + // If transport has just started or just stopped, + // update starting point: + if let Some(transport) = self.transport.as_ref() { + match (transport.query_state()?, started.as_ref()) { + (TransportState::Rolling, None) => { + let moment = Moment::zero(&self.timebase); + moment.sample.set(current_frames as f64); + moment.usec.set(current_usecs as f64); + *started = Some(moment); + }, + (TransportState::Stopped, Some(_)) => { + *started = None; + }, + _ => {} + }; + } + + self.playhead.update_from_sample(started.as_ref() + .map(|started|current_frames as f64 - started.sample.get()) + .unwrap_or(0.)); + + Ok(()) + } + + pub fn bbt (&self) -> PositionBBT { + let pulse = self.playhead.pulse.get() as i32; + let ppq = self.timebase.ppq.get() as i32; + let bpm = self.timebase.bpm.get(); + let bar = (pulse / ppq) / 4; + PositionBBT { + bar: 1 + bar, + beat: 1 + (pulse / ppq) % 4, + tick: (pulse % ppq), + bar_start_tick: (bar * 4 * ppq) as f64, + beat_type: 4., + beats_per_bar: 4., + beats_per_minute: bpm, + ticks_per_beat: ppq as f64 + } + } + + pub fn next_launch_instant (&self) -> Moment { + Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) + } + + /// Get index of first sample to populate. + /// + /// Greater than 0 means that the first pulse of the clip + /// falls somewhere in the middle of the chunk. + pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ + (scope.last_frame_time() as usize).saturating_sub( + started.sample.get() as usize + + self.started.read().unwrap().as_ref().unwrap().sample.get() as usize + ) + } + + // Get iterator that emits sample paired with pulse. + // + // * Sample: index into output buffer at which to write MIDI event + // * Pulse: index into clip from which to take the MIDI event + // + // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. + pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> Ticker { + self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) + } + } + impl Clock { + fn _todo_provide_u32 (&self) -> u32 { + todo!() + } + fn _todo_provide_opt_u32 (&self) -> Option { + todo!() + } + fn _todo_provide_f64 (&self) -> f64 { + todo!() + } + } + impl Command for ClockCommand { + fn execute (&self, state: &mut T) -> Perhaps { + self.execute(state.clock_mut()) // awesome + } + } + impl ClockView { + pub const BEAT_EMPTY: &'static str = "-.-.--"; + pub const TIME_EMPTY: &'static str = "-.---s"; + pub const BPM_EMPTY: &'static str = "---.---"; + pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { + let rate = clock.timebase.sr.get(); + let chunk = clock.chunk.load(Relaxed) as f64; + let lat = chunk / rate * 1000.; + let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); + let mut cache = cache.write().unwrap(); + cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); + cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); + cache.sr.update(Some((compact, rate)), |buf,_,_|{ + buf.clear(); + if compact { + write!(buf, "{:.1}kHz", rate / 1000.) + } else { + write!(buf, "{:.0}Hz", rate) + } + }); + if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { + let pulse = clock.timebase.usecs_to_pulse(now); + let time = now/1000000.; + let bpm = clock.timebase.bpm.get(); + cache.beat.update(Some(pulse), |buf, _, _|{ + buf.clear(); + clock.timebase.format_beats_1_to(buf, pulse) + }); + cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); + cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); + } else { + cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY)); + cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY)); + cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY)); + } + } + } + impl_default!(ClockView: { + let mut beat = String::with_capacity(16); + let _ = write!(beat, "{}", Self::BEAT_EMPTY); + let mut time = String::with_capacity(16); + let _ = write!(time, "{}", Self::TIME_EMPTY); + let mut bpm = String::with_capacity(16); + let _ = write!(bpm, "{}", Self::BPM_EMPTY); + Self { + beat: Memo::new(None, beat), + time: Memo::new(None, time), + bpm: Memo::new(None, bpm), + sr: Memo::new(None, String::with_capacity(16)), + buf: Memo::new(None, String::with_capacity(16)), + lat: Memo::new(None, String::with_capacity(16)), + } + }); diff --git a/src/connect.rs b/src/connect.rs deleted file mode 100644 index 97bae48b..00000000 --- a/src/connect.rs +++ /dev/null @@ -1,306 +0,0 @@ -use crate::*; - -/// Audio input port. -#[derive(Debug)] pub struct AudioInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// Audio output port. -#[derive(Debug)] pub struct AudioOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI input port. -#[derive(Debug)] pub struct MidiInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of currently held notes. - pub held: Arc>, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI output port. -#[derive(Debug)] pub struct MidiOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, - /// List of currently held notes. - pub held: Arc>, - /// Buffer - pub note_buffer: Vec, - /// Buffer - pub output_buffer: Vec>>, -} - -#[derive(Clone, Debug, PartialEq)] pub enum ConnectName { - /** Exact match */ - Exact(Arc), - /** Match regular expression */ - RegExp(Arc), -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { - One, - All -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { - Missing, - Disconnected, - Connected, - Mismatch, -} - -/// Port connection manager. -/// -/// ``` -/// let connect = tek::Connect::default(); -/// ``` -#[derive(Clone, Debug, Default)] pub struct Connect { - pub name: Option, - pub scope: Option, - pub status: Arc, Arc, ConnectStatus)>>>, - pub info: Arc, -} - -impl Connect { - pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) - -> Vec - { - let mut connections = vec![]; - for port in exact.iter() { connections.push(Self::exact(port)) } - for port in re.iter() { connections.push(Self::regexp(port)) } - for port in re_all.iter() { connections.push(Self::regexp_all(port)) } - connections - } - /// Connect to this exact port - pub fn exact (name: impl AsRef) -> Self { - let info = format!("=:{}", name.as_ref()).into(); - let name = Some(Exact(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp (name: impl AsRef) -> Self { - let info = format!("~:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp_all (name: impl AsRef) -> Self { - let info = format!("+:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn info (&self) -> Arc { - format!(" ({}) {} {}", { - let status = self.status.read().unwrap(); - let mut ok = 0; - for (_, _, state) in status.iter() { - if *state == Connected { - ok += 1 - } - } - format!("{ok}/{}", status.len()) - }, match self.scope { - None => "x", - Some(One) => " ", - Some(All) => "*", - }, match &self.name { - None => format!("x"), - Some(Exact(name)) => format!("= {name}"), - Some(RegExp(name)) => format!("~ {name}"), - }).into() - } -} - -impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl> RegisterPorts for J { - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiInput::new(self.jack(), name, connect) - } - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiOutput::new(self.jack(), name, connect) - } - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioInput::new(self.jack(), name, connect) - } - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioOutput::new(self.jack(), name, connect) - } -} - -/// May create new MIDI input ports. -pub trait AddMidiIn { - fn midi_in_add (&mut self) -> Usually<()>; -} - -/// May create new MIDI output ports. -pub trait AddMidiOut { - fn midi_out_add (&mut self) -> Usually<()>; -} - -pub trait RegisterPorts: HasJack<'static> { - /// Register a MIDI input port. - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register a MIDI output port. - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio input port. - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio output port. - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; -} - -pub trait JackPort: HasJack<'static> { - - type Port: PortSpec + Default; - - type Pair: PortSpec + Default; - - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized; - - fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { - jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) - .map_err(|e|e.into()) - } - - fn port_name (&self) -> &Arc; - - fn connections (&self) -> &[Connect]; - - fn port (&self) -> &Port; - - fn port_mut (&mut self) -> &mut Port; - - fn into_port (self) -> Port where Self: Sized; - - fn close (self) -> Usually<()> where Self: Sized { - let jack = self.jack().clone(); - Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) - } - - fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { - self.with_client(|c|c.ports(re_name, re_type, flags)) - } - - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - - fn port_by_name (&self, name: impl AsRef) -> Option> { - self.with_client(|c|c.port_by_name(name.as_ref())) - } - - fn connect_to_matching <'k> (&'k self) -> Usually<()> { - for connect in self.connections().iter() { - match &connect.name { - Some(Exact(name)) => { - *connect.status.write().unwrap() = self.connect_exact(name)?; - }, - Some(RegExp(re)) => { - *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; - }, - _ => {}, - }; - } - Ok(()) - } - - fn connect_exact <'k> (&'k self, name: &str) -> - Usually, Arc, ConnectStatus)>> - { - self.with_client(move|c|{ - let mut status = vec![]; - for port in c.ports(None, None, PortFlags::empty()).iter() { - if port.as_str() == &*name { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected { - break - } - } - } - } - Ok(status) - }) - } - - fn connect_regexp <'k> ( - &'k self, re: &str, scope: Option - ) -> Usually, Arc, ConnectStatus)>> { - self.with_client(move|c|{ - let mut status = vec![]; - let ports = c.ports(Some(&re), None, PortFlags::empty()); - for port in ports.iter() { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected && scope == Some(One) { - break - } - } - } - Ok(status) - }) - } - - /** Connect to a matching port by name. */ - fn connect_to_name (&self, name: impl AsRef) -> Usually { - self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { - self.connect_to_unowned(port) - } else { - Ok(Missing) - }) - } - - /** Connect to a matching port by reference. */ - fn connect_to_unowned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } - - /** Connect to an owned matching port by reference. */ - fn connect_to_owned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } - -} diff --git a/src/device.rs b/src/device.rs index e69de29b..bd44bf77 100644 --- a/src/device.rs +++ b/src/device.rs @@ -0,0 +1,583 @@ + +impl Device { + pub fn name (&self) -> &str { + match self { + Self::Sampler(sampler) => sampler.name.as_ref(), + _ => todo!(), + } + } + pub fn midi_ins (&self) -> &[MidiInput] { + match self { + //Self::Sampler(Sampler { midi_in, .. }) => &[midi_in], + _ => todo!() + } + } + pub fn midi_outs (&self) -> &[MidiOutput] { + match self { + Self::Sampler(_) => &[], + _ => todo!() + } + } + pub fn audio_ins (&self) -> &[AudioInput] { + match self { + Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(), + _ => todo!() + } + } + pub fn audio_outs (&self) -> &[AudioOutput] { + match self { + Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(), + _ => todo!() + } + } +} + +/// A device that can be plugged into the chain. +/// +/// ``` +/// let device = tek::Device::default(); +/// ``` +#[derive(Debug, Default)] pub enum Device { + #[default] + Bypass, + Mute, + #[cfg(feature = "sampler")] + Sampler(Sampler), + #[cfg(feature = "lv2")] // TODO + Lv2(Lv2), + #[cfg(feature = "vst2")] // TODO + Vst2, + #[cfg(feature = "vst3")] // TODO + Vst3, + #[cfg(feature = "clap")] // TODO + Clap, + #[cfg(feature = "sf2")] // TODO + Sf2, +} + +/// Some sort of wrapper? +pub struct DeviceAudio<'a>(pub &'a mut Device); + +/// Audio input port. +#[derive(Debug)] pub struct AudioInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +/// Audio output port. +#[derive(Debug)] pub struct AudioOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +/// MIDI input port. +#[derive(Debug)] pub struct MidiInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of currently held notes. + pub held: Arc>, + /// List of ports to connect to. + pub connections: Vec, +} + +/// MIDI output port. +#[derive(Debug)] pub struct MidiOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, + /// List of currently held notes. + pub held: Arc>, + /// Buffer + pub note_buffer: Vec, + /// Buffer + pub output_buffer: Vec>>, +} + +#[derive(Clone, Debug, PartialEq)] pub enum ConnectName { + /** Exact match */ + Exact(Arc), + /** Match regular expression */ + RegExp(Arc), +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { + One, + All +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { + Missing, + Disconnected, + Connected, + Mismatch, +} + +/// Port connection manager. +/// +/// ``` +/// let connect = tek::Connect::default(); +/// ``` +#[derive(Clone, Debug, Default)] pub struct Connect { + pub name: Option, + pub scope: Option, + pub status: Arc, Arc, ConnectStatus)>>>, + pub info: Arc, +} + +impl Connect { + pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) + -> Vec + { + let mut connections = vec![]; + for port in exact.iter() { connections.push(Self::exact(port)) } + for port in re.iter() { connections.push(Self::regexp(port)) } + for port in re_all.iter() { connections.push(Self::regexp_all(port)) } + connections + } + /// Connect to this exact port + pub fn exact (name: impl AsRef) -> Self { + let info = format!("=:{}", name.as_ref()).into(); + let name = Some(Exact(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp (name: impl AsRef) -> Self { + let info = format!("~:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp_all (name: impl AsRef) -> Self { + let info = format!("+:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn info (&self) -> Arc { + format!(" ({}) {} {}", { + let status = self.status.read().unwrap(); + let mut ok = 0; + for (_, _, state) in status.iter() { + if *state == Connected { + ok += 1 + } + } + format!("{ok}/{}", status.len()) + }, match self.scope { + None => "x", + Some(One) => " ", + Some(All) => "*", + }, match &self.name { + None => format!("x"), + Some(Exact(name)) => format!("= {name}"), + Some(RegExp(name)) => format!("~ {name}"), + }).into() + } +} + +impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl> RegisterPorts for J { + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiInput::new(self.jack(), name, connect) + } + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiOutput::new(self.jack(), name, connect) + } + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioInput::new(self.jack(), name, connect) + } + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioOutput::new(self.jack(), name, connect) + } +} + +/// May create new MIDI input ports. +pub trait AddMidiIn { + fn midi_in_add (&mut self) -> Usually<()>; +} + +/// May create new MIDI output ports. +pub trait AddMidiOut { + fn midi_out_add (&mut self) -> Usually<()>; +} + +pub trait RegisterPorts: HasJack<'static> { + /// Register a MIDI input port. + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register a MIDI output port. + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio input port. + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio output port. + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; +} + +pub trait JackPort: HasJack<'static> { + + type Port: PortSpec + Default; + + type Pair: PortSpec + Default; + + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized; + + fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { + jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) + .map_err(|e|e.into()) + } + + fn port_name (&self) -> &Arc; + + fn connections (&self) -> &[Connect]; + + fn port (&self) -> &Port; + + fn port_mut (&mut self) -> &mut Port; + + fn into_port (self) -> Port where Self: Sized; + + fn close (self) -> Usually<()> where Self: Sized { + let jack = self.jack().clone(); + Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) + } + + fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { + self.with_client(|c|c.ports(re_name, re_type, flags)) + } + + fn port_by_id (&self, id: u32) -> Option> { + self.with_client(|c|c.port_by_id(id)) + } + + fn port_by_name (&self, name: impl AsRef) -> Option> { + self.with_client(|c|c.port_by_name(name.as_ref())) + } + + fn connect_to_matching <'k> (&'k self) -> Usually<()> { + for connect in self.connections().iter() { + match &connect.name { + Some(Exact(name)) => { + *connect.status.write().unwrap() = self.connect_exact(name)?; + }, + Some(RegExp(re)) => { + *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; + }, + _ => {}, + }; + } + Ok(()) + } + + fn connect_exact <'k> (&'k self, name: &str) -> + Usually, Arc, ConnectStatus)>> + { + self.with_client(move|c|{ + let mut status = vec![]; + for port in c.ports(None, None, PortFlags::empty()).iter() { + if port.as_str() == &*name { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected { + break + } + } + } + } + Ok(status) + }) + } + + fn connect_regexp <'k> ( + &'k self, re: &str, scope: Option + ) -> Usually, Arc, ConnectStatus)>> { + self.with_client(move|c|{ + let mut status = vec![]; + let ports = c.ports(Some(&re), None, PortFlags::empty()); + for port in ports.iter() { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected && scope == Some(One) { + break + } + } + } + Ok(status) + }) + } + + /** Connect to a matching port by name. */ + fn connect_to_name (&self, name: impl AsRef) -> Usually { + self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { + self.connect_to_unowned(port) + } else { + Ok(Missing) + }) + } + + /** Connect to a matching port by reference. */ + fn connect_to_unowned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } + + /** Connect to an owned matching port by reference. */ + fn connect_to_owned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } +} + +impl JackPort for MidiInput { + type Port = MidiIn; + type Pair = MidiOut; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec(), + held: Arc::new(RwLock::new([false;128])) + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl JackPort for MidiOutput { + type Port = MidiOut; + type Pair = MidiIn; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self::register(jack, name)?; + let jack = jack.clone(); + let name = name.as_ref().into(); + let connections = connect.to_vec(); + let port = Self { + jack, + port, + name, + connections, + held: Arc::new([false;128].into()), + note_buffer: vec![0;8], + output_buffer: vec![vec![];65536], + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl MidiOutput { + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); + for frame in &mut self.output_buffer[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(&mut self.output_buffer); + } + } + /// Write a note to the output buffer + pub fn buffer_write <'a> ( + &'a mut self, + sample: usize, + event: LiveEvent, + ) { + self.note_buffer.fill(0); + event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); + self.output_buffer[sample].push(self.note_buffer.clone()); + // Update the list of currently held notes. + if let LiveEvent::Midi { ref message, .. } = event { + update_keys(&mut*self.held.write().unwrap(), message); + } + } + /// Write a chunk of MIDI data from the output buffer to the output port. + pub fn buffer_emit (&mut self, scope: &ProcessScope) { + let samples = scope.n_frames() as usize; + let mut writer = self.port.writer(scope); + for (time, events) in self.output_buffer.iter().enumerate().take(samples) { + for bytes in events.iter() { + writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ + panic!("Failed to write MIDI data: {bytes:?}"); + }); + } + } + } +} +impl MidiInput { + pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { + parse_midi_input(self.port().iter(scope)) + } +} +impl> + AsMut>> HasMidiIns for T { + fn midi_ins (&self) -> &Vec { self.as_ref() } + fn midi_ins_mut (&mut self) -> &mut Vec { self.as_mut() } +} +impl> + AsMut>> HasMidiOuts for T { + fn midi_outs (&self) -> &Vec { self.as_ref() } + fn midi_outs_mut (&mut self) -> &mut Vec { self.as_mut() } +} +impl> AddMidiIn for T { + fn midi_in_add (&mut self) -> Usually<()> { + let index = self.midi_ins().len(); + let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; + self.midi_ins_mut().push(port); + Ok(()) + } +} +/// Trail for thing that may gain new MIDI ports. +impl> AddMidiOut for T { + fn midi_out_add (&mut self) -> Usually<()> { + let index = self.midi_outs().len(); + let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; + self.midi_outs_mut().push(port); + Ok(()) + } +} + +impl JackPort for AudioInput { + type Port = AudioIn; + type Pair = AudioOut; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec() + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl JackPort for AudioOutput { + type Port = AudioOut; + type Pair = AudioIn; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec() + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl_audio!(|self: DeviceAudio<'a>, client, scope|{ + use Device::*; + match self.0 { + Mute => { Control::Continue }, + Bypass => { /*TODO*/ Control::Continue }, + #[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope), + #[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope), + #[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO + #[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO + #[cfg(feature = "clap")] Clap => { todo!() }, // TODO + #[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO + } +}); diff --git a/src/mix.rs b/src/mix.rs index 63248378..4e3a0ec2 100644 --- a/src/mix.rs +++ b/src/mix.rs @@ -12,3 +12,59 @@ Average, } + +#[cfg(test)] mod test_view_meter { + use super::*; + use proptest::prelude::*; + proptest! { + + #[test] fn proptest_view_meter ( + label in "\\PC*", value in f32::MIN..f32::MAX + ) { + let _ = view_meter(&label, value); + } + + #[test] fn proptest_view_meters ( + value1 in f32::MIN..f32::MAX, + value2 in f32::MIN..f32::MAX + ) { + let _ = view_meters(&[value1, value2]); + } + } +} + + impl Draw for RmsMeter { + fn draw(self, to: &mut Tui) -> Usually> { + let XYWH(x, y, w, h) = to.area(); + let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); + } + } + } + } + + impl Draw for Log10Meter { + fn draw(self, to: &mut Tui) -> Usually> { + let XYWH(x, y, w, h) = to.area(); + let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32 / 100.0).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2 - y, Some(Style::default().green())); + } + } + } + } + + fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { + Tui::bg(Black, w_exact(2, iter_east(1, ||meters.iter(), |value, _index|{ + h_full(RmsMeter(*value)) + }))) + } diff --git a/src/plugin.rs b/src/plugin.rs index 01d287f4..13d9aae8 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -26,3 +26,153 @@ pub window: Option } +impl_audio!(Lv2: lv2_jack_process); + +impl Lv2 { + const INPUT_BUFFER: usize = 1024; + pub fn new ( + jack: &Jack<'static>, + name: &str, + uri: &str, + ) -> Usually { + let lv2_world = livi::World::with_load_bundle(&uri); + let lv2_features = lv2_world.build_features(livi::FeaturesBuilder { + min_block_length: 1, + max_block_length: 65536, + }); + let lv2_plugin = lv2_world.iter_plugins().nth(0) + .unwrap_or_else(||panic!("plugin not found: {uri}")); + Ok(Self { + jack: jack.clone(), + name: name.into(), + path: Some(String::from(uri).into()), + selected: 0, + mapping: false, + midi_ins: vec![], + midi_outs: vec![], + audio_ins: vec![], + audio_outs: vec![], + lv2_instance: unsafe { + lv2_plugin + .instantiate(lv2_features.clone(), 48000.0) + .expect(&format!("instantiate failed: {uri}")) + }, + lv2_port_list: lv2_plugin.ports().collect::>(), + lv2_input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), + lv2_ui_thread: None, + lv2_world, + lv2_features, + lv2_plugin, + }) + } +} +fn lv2_jack_process ( + Lv2 { + midi_ins, midi_outs, audio_ins, audio_outs, + lv2_features, lv2_instance, lv2_input_buffer, .. + }: &mut Lv2, + _client: &Client, + scope: &ProcessScope +) -> Control { + let urid = lv2_features.midi_urid(); + lv2_input_buffer.clear(); + for port in midi_ins.iter() { + let mut atom = ::livi::event::LV2AtomSequence::new( + &lv2_features, + scope.n_frames() as usize + ); + for event in port.iter(scope) { + match event.bytes.len() { + 3 => atom.push_midi_event::<3>( + event.time as i64, + urid, + &event.bytes[0..3] + ).unwrap(), + _ => {} + } + } + lv2_input_buffer.push(atom); + } + let mut outputs = vec![]; + for _ in midi_outs.iter() { + outputs.push(::livi::event::LV2AtomSequence::new( + lv2_features, + scope.n_frames() as usize + )); + } + let ports = ::livi::EmptyPortConnections::new() + .with_atom_sequence_inputs(lv2_input_buffer.iter()) + .with_atom_sequence_outputs(outputs.iter_mut()) + .with_audio_inputs(audio_ins.iter().map(|o|o.as_slice(scope))) + .with_audio_outputs(audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); + unsafe { + lv2_instance.run(scope.n_frames() as usize, ports).unwrap() + }; + Control::Continue +} + +impl LV2PluginUI { pub fn new () -> Usually { Ok(Self { window: None }) } } + +impl ApplicationHandler for LV2PluginUI { + fn resumed (&mut self, event_loop: &ActiveEventLoop) { + self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); + } + fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { + match event { + WindowEvent::CloseRequested => { + self.window.as_ref().unwrap().set_visible(false); + event_loop.exit(); + }, + WindowEvent::RedrawRequested => { + self.window.as_ref().unwrap().request_redraw(); + } + _ => (), + } + } +} + +impl Draw for Lv2 { + fn draw(self, to: &mut Tui) { + let area = to.area(); + let XYWH(x, y, _, height) = area; + let mut width = 20u16; + let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1)); + let end = start + height as usize - 2; + //draw_box(buf, Rect { x, y, width, height }); + for i in start..end { + if let Some(port) = self.lv2_port_list.get(i) { + let value = if let Some(value) = self.lv2_instance.control_input(port.index) { + value + } else { + port.default_value + }; + //let label = &format!("C·· M·· {:25} = {value:.03}", port.name); + let label = &format!("{:25} = {value:.03}", port.name); + width = width.max(label.len() as u16 + 4); + let style = if i == self.selected { + Some(Style::default().green()) + } else { + None + } ; + to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style); + } else { + break + } + } + draw_header(self, to, x, y, width); + } +} + + +fn draw_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) { + let style = Style::default().gray(); + let label1 = format!(" {}", state.name); + to.blit(&label1, x + 1, y, Some(style.white().bold())); + if let Some(ref path) = state.path { + let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]); + to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim())); + } + //Ok(Rect { x, y, width: w, height: 1 }) +} + +#[cfg(feature = "vst2")] impl ::vst::host::Host for Plugin {} diff --git a/src/sample.rs b/src/sample.rs index 773644cc..74fba95c 100644 --- a/src/sample.rs +++ b/src/sample.rs @@ -1,3 +1,6 @@ +use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; +use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput}; + /// Plays [Voice]s from [Sample]s. /// /// ``` @@ -102,3 +105,549 @@ pub type MidiSample = (Option, Arc>); + +impl Default for SampleKit { + fn default () -> Self { Self([const { None }; N]) } +} +impl Iterator for Voice { + type Item = [f32;2]; + fn next (&mut self) -> Option { + if self.after > 0 { + self.after -= 1; + return Some([0.0, 0.0]) + } + let sample = self.sample.read().unwrap(); + if self.position < sample.end { + let position = self.position; + self.position += 1; + return sample.channels[0].get(position).map(|_amplitude|[ + sample.channels[0][position] * self.velocity * sample.gain, + sample.channels[0][position] * self.velocity * sample.gain, + ]) + } + None + } +} +impl NoteRange for Sampler { + fn note_lo (&self) -> &AtomicUsize { + &self.note_lo + } + fn note_axis (&self) -> &AtomicUsize { + &self.size.y + } +} +impl NotePoint for Sampler { + fn note_len (&self) -> &AtomicUsize { + unreachable!(); + } + fn get_note_len (&self) -> usize { + 0 + } + fn set_note_len (&self, _x: usize) -> usize { + 0 /*TODO?*/ + } + fn note_pos (&self) -> &AtomicUsize { + &self.note_pt + } + fn get_note_pos (&self) -> usize { + self.note_pt.load(Relaxed) + } + fn set_note_pos (&self, x: usize) -> usize { + let old = self.note_pt.swap(x, Relaxed); + self.cursor.0.store(x % 8, Relaxed); + self.cursor.1.store(x / 8, Relaxed); + old + } +} +impl Sampler { + pub fn new ( + jack: &Jack<'static>, + name: impl AsRef, + #[cfg(feature = "port")] midi_from: &[Connect], + #[cfg(feature = "port")] audio_from: &[&[Connect];2], + #[cfg(feature = "port")] audio_to: &[&[Connect];2], + ) -> Usually { + let name = name.as_ref(); + Ok(Self { + name: name.into(), + input_meters: vec![0.0;2], + output_meters: vec![0.0;2], + output_gain: 1., + buffer: vec![vec![0.0;16384];2], + #[cfg(feature = "port")] midi_in: Some( + MidiInput::new(jack, &format!("M/{name}"), midi_from)? + ), + #[cfg(feature = "port")] audio_ins: vec![ + AudioInput::new(jack, &format!("L/{name}"), audio_from[0])?, + AudioInput::new(jack, &format!("R/{name}"), audio_from[1])?, + ], + #[cfg(feature = "port")] audio_outs: vec![ + AudioOutput::new(jack, &format!("{name}/L"), audio_to[0])?, + AudioOutput::new(jack, &format!("{name}/R"), audio_to[1])?, + ], + ..Default::default() + }) + } + /// Value of cursor + pub fn cursor (&self) -> (usize, usize) { + (self.cursor.0.load(Relaxed), self.cursor.1.load(Relaxed)) + } + fn sample_selected (&self) -> usize { + (self.get_note_pos() as u8).into() + } + fn sample_selected_pitch (&self) -> u7 { + (self.get_note_pos() as u8).into() + } + pub fn process_audio_in (&mut self, scope: &ProcessScope) { + self.reset_input_meters(); + if self.recording.is_some() { + self.record_into(scope); + } else { + self.update_input_meters(scope); + } + } + /// Make sure that input meter count corresponds to input channel count + fn reset_input_meters (&mut self) { + let channels = self.audio_ins.len(); + if self.input_meters.len() != channels { + self.input_meters = vec![f32::MIN;channels]; + } + } + /// Record from inputs to sample + fn record_into (&mut self, scope: &ProcessScope) { + if let Some(ref sample) = self.recording.as_ref().expect("no recording sample").1 { + let mut sample = sample.write().unwrap(); + if sample.channels.len() != self.audio_ins.len() { + panic!("channel count mismatch"); + } + let samples_with_meters = self.audio_ins.iter() + .zip(self.input_meters.iter_mut()) + .zip(sample.channels.iter_mut()); + let mut length = 0; + for ((input, meter), channel) in samples_with_meters { + let slice = input.port().as_slice(scope); + length = length.max(slice.len()); + *meter = to_rms(slice); + channel.extend_from_slice(slice); + } + sample.end += length; + } else { + panic!("tried to record into the void") + } + } + /// Update input meters + fn update_input_meters (&mut self, scope: &ProcessScope) { + for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { + let slice = input.port().as_slice(scope); + *meter = to_rms(slice); + } + } + /// Make sure that output meter count corresponds to input channel count + fn reset_output_meters (&mut self) { + let channels = self.audio_outs.len(); + if self.output_meters.len() != channels { + self.output_meters = vec![f32::MIN;channels]; + } + } + /// Mix all currently playing samples into the output. + pub fn process_audio_out (&mut self, scope: &ProcessScope) { + self.clear_output_buffer(); + self.populate_output_buffer(scope.n_frames() as usize); + self.write_output_buffer(scope); + } + /// Zero the output buffer. + fn clear_output_buffer (&mut self) { + for buffer in self.buffer.iter_mut() { + buffer.fill(0.0); + } + } + /// Write playing voices to output buffer + fn populate_output_buffer (&mut self, frames: usize) { + let Sampler { buffer, voices, output_gain, mixing_mode, .. } = self; + let _channel_count = buffer.len(); + match mixing_mode { + MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{ + mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) + }), + MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{ + mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) + }), + } + } + /// Write output buffer to output ports. + fn write_output_buffer (&mut self, scope: &ProcessScope) { + let Sampler { audio_outs, buffer, .. } = self; + for (i, port) in audio_outs.iter_mut().enumerate() { + let buffer = &buffer[i]; + for (i, value) in port.port_mut().as_mut_slice(scope).iter_mut().enumerate() { + *value = *buffer.get(i).unwrap_or(&0.0); + } + } + } +} +impl SampleAdd { + fn exited (&self) -> bool { + self.exited + } + fn exit (&mut self) { + self.exited = true + } + pub fn new ( + sample: &Arc>, + voices: &Arc>> + ) -> Usually { + let dir = std::env::current_dir()?; + let (subdirs, files) = scan(&dir)?; + Ok(Self { + exited: false, + dir, + subdirs, + files, + cursor: 0, + offset: 0, + sample: sample.clone(), + voices: voices.clone(), + _search: None + }) + } + fn rescan (&mut self) -> Usually<()> { + scan(&self.dir).map(|(subdirs, files)|{ + self.subdirs = subdirs; + self.files = files; + }) + } + fn prev (&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + fn next (&mut self) { + self.cursor = self.cursor + 1; + } + fn try_preview (&mut self) -> Usually<()> { + if let Some(path) = self.cursor_file() { + if let Ok(sample) = Sample::from_file(&path) { + *self.sample.write().unwrap() = sample; + self.voices.write().unwrap().push( + Sample::play(&self.sample, 0, &u7::from(100u8)) + ); + } + //load_sample(&path)?; + //let src = std::fs::File::open(&path)?; + //let mss = MediaSourceStream::new(Box::new(src), Default::default()); + //let mut hint = Hint::new(); + //if let Some(ext) = path.extension() { + //hint.with_extension(&ext.to_string_lossy()); + //} + //let meta_opts: MetadataOptions = Default::default(); + //let fmt_opts: FormatOptions = Default::default(); + //if let Ok(mut probed) = symphonia::default::get_probe() + //.format(&hint, mss, &fmt_opts, &meta_opts) + //{ + //panic!("{:?}", probed.format.metadata()); + //}; + } + Ok(()) + } + fn cursor_dir (&self) -> Option { + if self.cursor < self.subdirs.len() { + Some(self.dir.join(&self.subdirs[self.cursor])) + } else { + None + } + } + fn cursor_file (&self) -> Option { + if self.cursor < self.subdirs.len() { + return None + } + let index = self.cursor.saturating_sub(self.subdirs.len()); + if index < self.files.len() { + Some(self.dir.join(&self.files[index])) + } else { + None + } + } + fn pick (&mut self) -> Usually { + if self.cursor == 0 { + if let Some(parent) = self.dir.parent() { + self.dir = parent.into(); + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + } + if let Some(dir) = self.cursor_dir() { + self.dir = dir; + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + if let Some(path) = self.cursor_file() { + let (end, channels) = read_sample_data(&path.to_string_lossy())?; + let mut sample = self.sample.write().unwrap(); + sample.name = path.file_name().unwrap().to_string_lossy().into(); + sample.end = end; + sample.channels = channels; + return Ok(true) + } + return Ok(false) + } +} +impl SampleKit { + pub fn get (&self, index: usize) -> &Option>> { + if index < self.0.len() { + &self.0[index] + } else { + &None + } + } +} +impl Sample { + pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { + Self { + name: name.as_ref().into(), + start, + end, + channels, + rate: None, + gain: 1.0, + color: ItemTheme::random(), + } + } + pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { + Voice { + sample: sample.clone(), + after, + position: sample.read().unwrap().start, + velocity: velocity.as_int() as f32 / 127.0, + } + } + pub fn handle_cc (&mut self, controller: u7, value: u7) { + let percentage = value.as_int() as f64 / 127.; + match controller.as_int() { + 20 => { + self.start = (percentage * self.end as f64) as usize; + }, + 21 => { + let length = self.channels[0].len(); + self.end = length.min( + self.start + (percentage * (length as f64 - self.start as f64)) as usize + ); + }, + 22 => { /*attack*/ }, + 23 => { /*decay*/ }, + 24 => { + self.gain = percentage as f32 * 2.0; + }, + 26 => { /* pan */ } + 25 => { /* pitch */ } + _ => {} + } + } + /// Read WAV from file + pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { + let mut channels: Vec> = vec![]; + for channel in wavers::Wav::from_path(src)?.channels() { + channels.push(channel); + } + let mut end = 0; + let mut data: Vec> = vec![]; + for samples in channels.iter() { + let channel = Vec::from(samples.as_ref()); + end = end.max(channel.len()); + data.push(channel); + } + Ok((end, data)) + } + pub fn from_file (path: &PathBuf) -> Usually { + let name = path.file_name().unwrap().to_string_lossy().into(); + let mut sample = Self { name, ..Default::default() }; + // Use file extension if present + let mut hint = Hint::new(); + if let Some(ext) = path.extension() { + hint.with_extension(&ext.to_string_lossy()); + } + let probed = symphonia::default::get_probe().format( + &hint, + MediaSourceStream::new( + Box::new(File::open(path)?), + Default::default(), + ), + &Default::default(), + &Default::default() + )?; + let mut format = probed.format; + let params = &format.tracks().iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .expect("no tracks found") + .codec_params; + let mut decoder = get_codecs().make(params, &Default::default())?; + loop { + match format.next_packet() { + Ok(packet) => sample.decode_packet(&mut decoder, packet)?, + Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), + Err(err) => return Err(err.into()), + }; + }; + sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); + Ok(sample) + } + fn decode_packet ( + &mut self, decoder: &mut Box, packet: Packet + ) -> Usually<()> { + // Decode a packet + let decoded = decoder + .decode(&packet) + .map_err(|e|Box::::from(e))?; + // Determine sample rate + let spec = *decoded.spec(); + if let Some(rate) = self.rate { + if rate != spec.rate as usize { + panic!("sample rate changed"); + } + } else { + self.rate = Some(spec.rate as usize); + } + // Determine channel count + while self.channels.len() < spec.channels.count() { + self.channels.push(vec![]); + } + // Load sample + let mut samples = SampleBuffer::new( + decoded.frames() as u64, + spec + ); + if samples.capacity() > 0 { + samples.copy_interleaved_ref(decoded); + for frame in samples.samples().chunks(spec.channels.count()) { + for (chan, frame) in frame.iter().enumerate() { + self.channels[chan].push(*frame) + } + } + } + Ok(()) + } +} +impl Draw for SampleAdd { + fn draw (self, _to: &mut Tui) -> Usually> { + todo!() + } +} + +fn draw_list_item (sample: &Option>>) -> String { + if let Some(sample) = sample { + let sample = sample.read().unwrap(); + format!("{:8}", sample.name) + //format!("{:8} {:3} {:6}-{:6}/{:6}", + //sample.name, + //sample.gain, + //sample.start, + //sample.end, + //sample.channels[0].len() + //) + } else { + String::from("........") + } +} + +fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + use<'_> { + let min_db = -64.0; + Thunk::new(move|to: &mut Tui|{ + let XYWH(x, y, width, height) = to.area(); + let area = Rect { x, y, width, height }; + if let Some(sample) = &sample { + let sample = sample.read().unwrap(); + let start = sample.start as f64; + let end = sample.end as f64; + let length = end - start; + let step = length / width as f64; + let mut t = start; + let mut lines = vec![]; + while t < end { + let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; + let total: f32 = chunk.iter().map(|x|x.abs()).sum(); + let count = chunk.len() as f32; + let meter = 10. * (total / count).log10(); + let x = t as f64; + let y = meter as f64; + lines.push(Line::new(x, min_db, x, y, Color::Green)); + t += step / 2.; + } + Canvas::default() + .x_bounds([sample.start as f64, sample.end as f64]) + .y_bounds([min_db, 0.]) + .paint(|ctx| { + for line in lines.iter() { + ctx.draw(line); + } + //FIXME: proportions + //let text = "press record to finish sampling"; + //ctx.print( + //(width - text.len() as u16) as f64 / 2.0, + //height as f64 / 2.0, + //text.red() + //); + }).render(area, &mut to.buffer); + } else { + Canvas::default() + .x_bounds([0.0, width as f64]) + .y_bounds([0.0, height as f64]) + .paint(|_ctx| { + //let text = "press record to begin sampling"; + //ctx.print( + //(width - text.len() as u16) as f64 / 2.0, + //height as f64 / 2.0, + //text.red() + //); + }) + .render(area, &mut to.buffer); + } + }) +} + +impl_audio!(Sampler: sampler_jack_process); +pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control { + if let Some(midi_in) = &state.midi_in { + for midi in midi_in.port().iter(scope) { + sampler_midi_in(&state.samples, &state.voices, midi) + } + } + state.process_audio_out(scope); + state.process_audio_in(scope); + Control::Continue +} + +/// Create [Voice]s from [Sample]s in response to MIDI input. +fn sampler_midi_in ( + samples: &SampleKit<128>, voices: &Arc>>, RawMidi { time, bytes }: RawMidi +) { + if let Ok(LiveEvent::Midi { message, .. }) = LiveEvent::parse(bytes) { + match message { + MidiMessage::NoteOn { ref key, ref vel } => { + if let Some(sample) = samples.get(key.as_int() as usize) { + voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); + } + }, + MidiMessage::Controller { controller: _, value: _ } => { + // TODO + } + _ => {} + } + } +} + +fn draw_sample ( + to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool +) -> Usually { + let style = if focus { Style::default().green() } else { Style::default() }; + if focus { + to.blit(&"🬴", x+1, y, Some(style.bold())); + } + let label1 = format!("{:3} {:12}", + note.map(|n|n.to_string()).unwrap_or(String::default()), + sample.name); + let label2 = format!("{:>6} {:>6} +0.0", + sample.start, + sample.end); + to.blit(&label1, x+2, y, Some(style.bold())); + to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); + Ok(label1.len() + label2.len() + 4) +} diff --git a/src/sequence.rs b/src/sequence.rs index 049b6dae..1e55f0dc 100644 --- a/src/sequence.rs +++ b/src/sequence.rs @@ -1,3 +1,4 @@ +use ::std::sync::{Arc, RwLock}; /// Contains state for viewing and editing a clip. /// @@ -594,3 +595,822 @@ pub trait MidiRange: TimeRange + NoteRange {} // Lowest note displayed pub note_lo: Arc, } + +impl MidiClip { + pub fn new ( + name: impl AsRef, + looped: bool, + length: usize, + notes: Option, + color: Option, + ) -> Self { + Self { + uuid: uuid::Uuid::new_v4(), + name: name.as_ref().into(), + ppq: PPQ, + length, + notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), + looped, + loop_start: 0, + loop_length: length, + percussive: true, + color: color.unwrap_or_else(ItemTheme::random) + } + } + pub fn count_midi_messages (&self) -> usize { + let mut count = 0; + for tick in self.notes.iter() { + count += tick.len(); + } + count + } + pub fn set_length (&mut self, length: usize) { + self.length = length; + self.notes = vec![Vec::with_capacity(16);length]; + } + pub fn duplicate (&self) -> Self { + let mut clone = self.clone(); + clone.uuid = uuid::Uuid::new_v4(); + clone + } + pub fn toggle_loop (&mut self) { self.looped = !self.looped; } + pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { + if pulse >= self.length { panic!("extend clip first") } + self.notes[pulse].push(message); + } + /// Check if a range `start..end` contains MIDI Note On `k` + pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { + for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { + for event in events.iter() { + if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } + } + } + false + } + pub fn stop_all () -> Self { + Self::new( + "Stop", + false, + 1, + Some(vec![vec![MidiMessage::Controller { + controller: 123.into(), + value: 0.into() + }]]), + Some(ItemColor::from_tui(Color::Rgb(32, 32, 32)).into()) + ) + } +} + +impl PartialEq for MidiClip { + fn eq (&self, other: &Self) -> bool { + self.uuid == other.uuid + } +} + +impl Eq for MidiClip {} + +impl MidiClip { + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_bool_stub_ (&self) -> bool { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } +} +impl_has!(Sequencer: |self: Track| self.sequencer); +impl_has!(Clock: |self: Sequencer| self.clock); +impl_has!(Vec: |self: Sequencer| self.midi_ins); +impl_has!(Vec: |self: Sequencer| self.midi_outs); +impl_has!(Measure: |self: MidiEditor| self.size); +impl_has!(Measure: |self: PianoHorizontal| self.size); +impl_default!(Sequencer: Self { + clock: Clock::default(), + play_clip: None, + next_clip: None, + midi_ins: vec![], + midi_outs: vec![], + recording: false, + monitoring: true, + overdub: false, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + note_buf: vec![0;8], + midi_buf: vec![], + reset: true, +}); +impl Sequencer { + pub fn new ( + name: impl AsRef, + jack: &Jack<'static>, + #[cfg(feature = "clock")] clock: Option<&Clock>, + #[cfg(feature = "clip")] clip: Option<&Arc>>, + #[cfg(feature = "port")] midi_from: &[Connect], + #[cfg(feature = "port")] midi_to: &[Connect], + ) -> Usually { + let _name = name.as_ref(); + #[cfg(feature = "clock")] let clock = clock.cloned().unwrap_or_default(); + Ok(Self { + reset: true, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + #[cfg(feature = "port")] midi_ins: vec![MidiInput::new(jack, &format!("M/{}", name.as_ref()), midi_from)?,], + #[cfg(feature = "port")] midi_outs: vec![MidiOutput::new(jack, &format!("{}/M", name.as_ref()), midi_to)?, ], + #[cfg(feature = "clip")] play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), + #[cfg(feature = "clock")] clock, + ..Default::default() + }) + } + fn process_rolling (&mut self, scope: &ProcessScope) -> Control { + self.process_clear(scope, false); + // Write chunk of clip to output, handle switchover + if self.process_playback(scope) { + self.process_switchover(scope); + } + // Monitor input to output + self.process_monitoring(scope); + // Record and/or monitor input + self.process_recording(scope); + // Emit contents of MIDI buffers to JACK MIDI output ports. + self.midi_outs_emit(scope); + Control::Continue + } + fn process_stopped (&mut self, scope: &ProcessScope) -> Control { + if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { + self.process_monitoring(scope) + } + Control::Continue + } + fn process_monitoring (&mut self, scope: &ProcessScope) { + let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat + let monitoring = self.monitoring(); + for input in self.midi_ins.iter() { + for (sample, event, bytes) in input.parsed(scope) { + if let LiveEvent::Midi { message, .. } = event { + if monitoring { + self.midi_buf[sample].push(bytes.to_vec()); + } + // FIXME: don't lock on every event! + update_keys(&mut notes_in.write().unwrap(), &message); + } + } + } + } + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); + for frame in &mut self.midi_buf_mut()[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(self.midi_buf_mut()); + } + for port in self.midi_outs_mut().iter_mut() { + // Clear output buffer(s) + port.buffer_clear(scope, false); + } + } + fn process_recording (&mut self, scope: &ProcessScope) { + if self.monitoring() { + self.monitor(scope); + } + if let Some((started, ref clip)) = self.play_clip.clone() { + self.record_clip(scope, started, clip); + } + if let Some((_start_at, _clip)) = &self.next_clip() { + self.record_next(); + } + } + fn process_playback (&mut self, scope: &ProcessScope) -> bool { + // If a clip is playing, write a chunk of MIDI events from it to the output buffer. + // If no clip is playing, prepare for switchover immediately. + if let Some((started, clip)) = &self.play_clip { + // Length of clip, to repeat or stop on end. + let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); + // Index of first sample to populate. + let offset = self.clock().get_sample_offset(scope, &started); + // Write MIDI events from clip at sample offsets corresponding to pulses. + for (sample, pulse) in self.clock().get_pulses(scope, offset) { + // If a next clip is enqueued, and we're past the end of the current one, + // break the loop here (FIXME count pulse correctly) + let past_end = if clip.is_some() { pulse >= length } else { true }; + // Is it time for switchover? + if self.next_clip().is_some() && past_end { + return true + } + // If there's a currently playing clip, output notes from it to buffer: + if let Some(clip) = clip { + // Source clip from which the MIDI events will be taken. + let clip = clip.read().unwrap(); + // Clip with zero length is not processed + if clip.length > 0 { + // Current pulse index in source clip + let pulse = pulse % clip.length; + // Output each MIDI event from clip at appropriate frames of output buffer: + for message in clip.notes[pulse].iter() { + for port in self.midi_outs.iter_mut() { + port.buffer_write(sample, LiveEvent::Midi { + channel: 0.into(), /* TODO */ + message: *message + }); + } + } + } + } + } + false + } else { + true + } + } + /// Handle switchover from current to next playing clip. + fn process_switchover (&mut self, scope: &ProcessScope) { + let _midi_buf = self.midi_buf_mut(); + let sample0 = scope.last_frame_time() as usize; + //let samples = scope.n_frames() as usize; + if let Some((start_at, clip)) = &self.next_clip() { + let start = start_at.sample.get() as usize; + let sample = self.clock().started.read().unwrap() + .as_ref().unwrap().sample.get() as usize; + // If it's time to switch to the next clip: + if start <= sample0.saturating_sub(sample) { + // Samples elapsed since clip was supposed to start + let _skipped = sample0 - start; + // Switch over to enqueued clip + let started = Moment::from_sample(self.clock().timebase(), start as f64); + // Launch enqueued clip + *self.play_clip_mut() = Some((started, clip.clone())); + // Unset enqueuement (TODO: where to implement looping?) + *self.next_clip_mut() = None; + // Fill in remaining ticks of chunk from next clip. + self.process_playback(scope); + } + } + } +} +impl HasMidiBuffers for Sequencer { + fn note_buf_mut (&mut self) -> &mut Vec { &mut self.note_buf } + fn midi_buf_mut (&mut self) -> &mut Vec>> { &mut self.midi_buf } +} +impl std::fmt::Debug for Sequencer { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Sequencer") + .field("clock", &self.clock) + .field("play_clip", &self.play_clip) + .field("next_clip", &self.next_clip) + .finish() + } +} +impl MidiMonitor for Sequencer { + fn monitoring (&self) -> bool { self.monitoring } + fn monitoring_mut (&mut self) -> &mut bool { &mut self.monitoring } + fn notes_in (&self) -> &Arc> { &self.notes_in } +} +impl MidiRecord for Sequencer { + fn recording (&self) -> bool { self.recording } + fn recording_mut (&mut self) -> &mut bool { &mut self.recording } + fn overdub (&self) -> bool { self.overdub } + fn overdub_mut (&mut self) -> &mut bool { &mut self.overdub } +} +#[cfg(feature="clip")] impl HasPlayClip for Sequencer { + fn reset (&self) -> bool { self.reset } + fn reset_mut (&mut self) -> &mut bool { &mut self.reset } + fn play_clip (&self) -> &Option<(Moment, Option>>)> { + &self.play_clip + } + fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.play_clip + } + fn next_clip (&self) -> &Option<(Moment, Option>>)> { + &self.next_clip + } + fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.next_clip + } +} +/// JACK process callback for a sequencer's clip sequencer/recorder. +impl Audio for Sequencer { + fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + if self.clock().is_rolling() { + self.process_rolling(scope) + } else { + self.process_stopped(scope) + } + } +} +impl Draw for MidiEditor { + fn draw(self, to: &mut Tui) -> Usually> { + self.tui().draw(to) + } +} +impl Draw for PianoHorizontal { + fn draw(self, to: &mut Tui) -> Usually> { + self.tui().draw(to) + } +} +impl std::fmt::Debug for MidiEditor { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("MidiEditor").field("mode", &self.mode).finish() + } +} +impl_from!(MidiEditor: |clip: &Arc>| { + let model = Self::from(Some(clip.clone())); + model.redraw(); + model +}); +impl_from!(MidiEditor: |clip: Option>>| { + let mut model = Self::default(); + *model.clip_mut() = clip; + model.redraw(); + model +}); +impl_default!(MidiEditor: Self { + size: Measure::new(0, 0), mode: PianoHorizontal::new(None) +}); +impl_default!(OctaveVertical: Self { + on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] +}); +impl MidiEditor { + /// Put note at current position + pub fn put_note (&mut self, advance: bool) { + let mut redraw = false; + if let Some(clip) = self.clip() { + let mut clip = clip.write().unwrap(); + let note_start = self.get_time_pos(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let note_end = note_start + (note_len.saturating_sub(1)); + let key: u7 = u7::from(note_pos as u8); + let vel: u7 = 100.into(); + let length = clip.length; + let note_end = note_end % length; + let note_on = MidiMessage::NoteOn { key, vel }; + if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { + clip.notes[note_start].push(note_on); + } + let note_off = MidiMessage::NoteOff { key, vel }; + if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { + clip.notes[note_end].push(note_off); + } + if advance { + self.set_time_pos((note_end + 1) % clip.length); + } + redraw = true; + } + if redraw { + self.mode.redraw(); + } + } + fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } + fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } + fn note_length (&self) -> usize { self.get_note_len() } + fn note_pos (&self) -> usize { self.get_note_pos() } + fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } + fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } + fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } + fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } + fn note_len (&self) -> usize { self.get_note_len() } + fn note_len_next (&self) -> usize { self.get_note_len() + 1 } + fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } + fn note_range (&self) -> usize { self.get_note_axis() } + fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } + fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } + fn time_zoom (&self) -> usize { self.get_time_zoom() } + fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_lock (&self) -> bool { self.get_time_lock() } + fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } + fn time_pos (&self) -> usize { self.get_time_pos() } + fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } + fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } + fn time_pos_prev (&self) -> usize { + let step = self.get_note_len(); + self.get_time_pos().overflowing_sub(step) + .0.min(self.clip_length().saturating_sub(step)) + } + fn time_pos_prev_fine (&self) -> usize { + self.get_time_pos().overflowing_sub(1) + .0.min(self.clip_length().saturating_sub(1)) + } + pub fn clip_status (&self) -> impl Draw + '_ { + let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.name.clone(), clip.length, clip.looped) + } else { (ItemTheme::G[64], String::new().into(), 0, false) }; + w_exact(20, south!( + w_full(origin_w(east( + button_2("f2", "name ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), + w_full(origin_w(east( + button_2("l", "ength ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), + w_full(origin_w(east( + button_2("r", "epeat ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), + )) + } + pub fn edit_status (&self) -> impl Draw + '_ { + let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.length) + } else { (ItemTheme::G[64], 0) }; + let time_pos = self.get_time_pos(); + let time_zoom = self.get_time_zoom(); + let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; + let note_pos = self.get_note_pos(); + let note_name = format!("{:4}", note_pitch_to_name(note_pos)); + let note_pos = format!("{:>3}", note_pos); + let note_len = format!("{:>4}", self.get_note_len()); + w_exact(20, south!( + w_full(origin_w(east( + button_2("t", "ime ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), + format!("{length} /{time_zoom} +{time_pos} "))))))), + w_full(origin_w(east( + button_2("z", "lock ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), + format!("{time_lock}"))))))), + w_full(origin_w(east( + button_2("x", "note ", false), + w_full(origin_e(Tui::fg(Rgb(255, 255, 255), + format!("{note_name} {note_pos} {note_len}"))))))), + )) + } +} + +impl TimeRange for MidiEditor { + fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } +} + +impl NoteRange for MidiEditor { + fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } +} + +impl NotePoint for MidiEditor { + fn note_len (&self) -> &AtomicUsize { self.mode.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } +} + +impl TimePoint for MidiEditor { + fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } +} + +impl MidiViewer for MidiEditor { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } + fn redraw (&self) { self.mode.redraw() } + fn clip (&self) -> &Option>> { self.mode.clip() } + fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } + fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } +} + +impl MidiEditor { + fn tui (&self) -> impl Draw { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } +} + + +impl PianoHorizontal { + pub fn new (clip: Option<&Arc>>) -> Self { + let size = Measure::new(0, 0); + let mut range = MidiSelection::from((12, true)); + range.time_axis = size.x.clone(); + range.note_axis = size.y.clone(); + let piano = Self { + keys_width: 5, + size, + range, + buffer: RwLock::new(Default::default()).into(), + point: MidiCursor::default(), + clip: clip.cloned(), + color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), + }; + piano.redraw(); + piano + } +} + +impl PianoHorizontal { + fn tui (&self) -> impl Draw { + south( + east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), + east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(self.cursor())))), + ) + } +} + +impl PianoHorizontal { + /// Draw the piano roll background. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { + for (y, note) in (0..=127).rev().enumerate() { + for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { + let cell = buf.get_mut(x, y).unwrap(); + if note == (127-note_point) || time == time_point { + cell.set_bg(Rgb(0,0,0)); + } else { + cell.set_bg(clip.color.darkest.rgb); + } + if time % 384 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('│'); + } else if time % 96 == 0 { + cell.set_fg(clip.color.dark.rgb); + cell.set_char('╎'); + } else if time % note_len == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('┊'); + } else if (127 - note) % 12 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('='); + } else if (127 - note) % 6 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('—'); + } else { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('·'); + } + } + } + } + /// Draw the piano roll foreground. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { + let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); + let mut notes_on = [false;128]; + for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { + for (_y, note) in (0..=127).rev().enumerate() { + if let Some(cell) = buf.get_mut(x, note) { + if notes_on[note] { + cell.set_char('▂'); + cell.set_style(style); + } + } + } + let time_end = time_start + zoom; + for time in time_start..time_end.min(clip.length) { + for event in clip.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + if let Some(cell) = buf.get_mut(x, note) { + cell.set_char('█'); + cell.set_style(style); + } + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} + } + } + } + + } + } + fn notes (&self) -> impl Draw { + let time_start = self.get_time_start(); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + let buffer = self.buffer.clone(); + Thunk::new(move|to: &mut Tui|{ + let source = buffer.read().unwrap(); + let XYWH(x0, y0, w, _h) = to.area(); + //if h as usize != note_axis { + //panic!("area height mismatch: {h} <> {note_axis}"); + //} + for (area_x, screen_x) in (x0..x0+w).enumerate() { + for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { + let source_x = time_start + area_x; + let source_y = note_hi - area_y; + // TODO: enable loop rollover: + //let source_x = (time_start + area_x) % source.width.max(1); + //let source_y = (note_hi - area_y) % source.height.max(1); + let is_in_x = source_x < source.width; + let is_in_y = source_y < source.height; + if is_in_x && is_in_y { + if let Some(source_cell) = source.get(source_x, source_y) { + if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { + *cell = source_cell.clone(); + } + } + } + } + } + }) + } + fn cursor (&self) -> impl Draw { + let note_hi = self.get_note_hi(); + let note_lo = self.get_note_lo(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let time_pos = self.get_time_pos(); + let time_start = self.get_time_start(); + let time_zoom = self.get_time_zoom(); + let style = Some(Style::default().fg(self.color.lightest.rgb)); + Thunk::new(move|to: &mut Tui|{ + let XYWH(x0, y0, w, _) = to.area(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + if note == note_pos { + for x in 0..w { + let screen_x = x0 + x; + let time_1 = time_start + x as usize * time_zoom; + let time_2 = time_1 + time_zoom; + if time_1 <= time_pos && time_pos < time_2 { + to.blit(&"█", screen_x, screen_y, style); + let tail = note_len as u16 / time_zoom as u16; + for x_tail in (screen_x + 1)..(screen_x + tail) { + to.blit(&"▂", x_tail, screen_y, style); + } + break + } + } + break + } + } + }) + } + fn keys (&self) -> impl Draw { + let state = self; + let color = state.color; + let note_lo = state.get_note_lo(); + let note_hi = state.get_note_hi(); + let note_pos = state.get_note_pos(); + let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); + let off_style = Some(Style::default().fg(Tui::g(255))); + let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); + h_full(w_exact(self.keys_width, Thunk::new(move|to: &mut Tui|{ + let XYWH(x, y0, _w, _h) = to.area(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + to.blit(&to_key(note), x, screen_y, key_style); + if note > 127 { + continue + } + if note == note_pos { + to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style) + } else { + to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) + }; + } + }))) + } + fn timeline (&self) -> impl Draw + '_ { + w_full(h_exact(1, Thunk::new(move|to: &mut Tui|{ + let XYWH(x, y, w, _h) = to.area(); + let style = Some(Style::default().dim()); + let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { + let t = area_x as usize * self.time_zoom().get(); + if t < length { + to.blit(&"|", screen_x, y, style); + } + } + }))) + } +} + +impl TimeRange for PianoHorizontal { + fn time_len (&self) -> &AtomicUsize { self.range.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.range.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } +} + +impl NoteRange for PianoHorizontal { + fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } +} + +impl NotePoint for PianoHorizontal { + fn note_len (&self) -> &AtomicUsize { self.point.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } +} + +impl TimePoint for PianoHorizontal { + fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } +} + +impl MidiViewer for PianoHorizontal { + fn clip (&self) -> &Option>> { &self.clip } + fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } + /// Determine the required space to render the clip. + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { (clip.length / self.range.time_zoom().get(), 128) } + fn redraw(self) { + *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { + let clip = clip.read().unwrap(); + let buf_size = self.buffer_size(&clip); + let mut buffer = BigBuffer::from(buf_size); + let time_zoom = self.get_time_zoom(); + self.time_len().set(clip.length); + PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); + PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); + buffer + } else { + Default::default() + } + } + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); + self.redraw(); + } +} + +impl std::fmt::Debug for PianoHorizontal { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + let buffer = self.buffer.read().unwrap(); + f.debug_struct("PianoHorizontal") + .field("time_zoom", &self.range.time_zoom) + .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) + .finish() + } +} +impl OctaveVertical { + fn color (&self, pitch: usize) -> Color { + let pitch = pitch % 12; + self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] + } +} +impl OctaveVertical { + fn tui (&self) -> impl Draw { + east!( + Tui::fg_bg(self.color(0), self.color(1), "▙"), + Tui::fg_bg(self.color(2), self.color(3), "▙"), + Tui::fg_bg(self.color(4), self.color(5), "▌"), + Tui::fg_bg(self.color(6), self.color(7), "▟"), + Tui::fg_bg(self.color(8), self.color(9), "▟"), + Tui::fg_bg(self.color(10), self.color(11), "▟"), + ) + } +} +impl_from!(MidiSelection: |data:(usize, bool)| Self { + time_len: Arc::new(0.into()), + note_axis: Arc::new(0.into()), + note_lo: Arc::new(0.into()), + time_axis: Arc::new(0.into()), + time_start: Arc::new(0.into()), + time_zoom: Arc::new(data.0.into()), + time_lock: Arc::new(data.1.into()), +}); +impl_default!(MidiCursor: Self { + time_pos: Arc::new(0.into()), + note_pos: Arc::new(36.into()), + note_len: Arc::new(24.into()), +}); + +impl NotePoint for MidiCursor { + fn note_len (&self) -> &AtomicUsize { + &self.note_len + } + fn note_pos (&self) -> &AtomicUsize { + &self.note_pos + } +} + +impl TimePoint for MidiCursor { + fn time_pos (&self) -> &AtomicUsize { + self.time_pos.as_ref() + } +} + +impl TimeRange for MidiSelection { + fn time_len (&self) -> &AtomicUsize { &self.time_len } + fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } + fn time_lock (&self) -> &AtomicBool { &self.time_lock } + fn time_start (&self) -> &AtomicUsize { &self.time_start } + fn time_axis (&self) -> &AtomicUsize { &self.time_axis } +} + +impl NoteRange for MidiSelection { + fn note_lo (&self) -> &AtomicUsize { &self.note_lo } + fn note_axis (&self) -> &AtomicUsize { &self.note_axis } +} + +impl Iterator for Ticker { + type Item = (usize, usize); + fn next (&mut self) -> Option { + loop { + if self.sample > self.end { return None } + let spp = self.spp; + let sample = self.sample as f64; + let start = self.start; + let end = self.end; + self.sample += 1; + //println!("{spp} {sample} {start} {end}"); + let jitter = sample.rem_euclid(spp); // ramps + let next_jitter = (sample + 1.0).rem_euclid(spp); + if jitter > next_jitter { // at crossing: + let time = (sample as usize) % (end as usize-start as usize); + let tick = (sample / spp) as usize; + return Some((time, tick)) + } + } + } +} diff --git a/src/tek.rs b/src/tek.rs index dc2c3169..8e04a885 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -4,15 +4,48 @@ impl_trait_in_assoc_type, trait_alias, type_alias_impl_trait, type_changing_struct_update )] +/// Implement an arithmetic operation for a unit of time +#[macro_export] macro_rules! impl_op { + ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { + let $a = self.get(); let $b = other.get(); Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { + let $a = self.get(); let $b = other as f64; Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { + let $a = self.get(); let $b = other; Self($impl.into()) + } + } + } +} + +#[macro_export] macro_rules! impl_has_clips { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { + fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { + $cb.read().unwrap() + } + fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { + $cb.write().unwrap() + } + } + } +} + pub mod arrange; pub mod browse; -pub mod connect; +pub mod clock; pub mod device; pub mod mix; pub mod plugin; pub mod sample; pub mod sequence; -pub mod tick; use clap::{self, Parser, Subcommand}; use builder_pattern::Builder; @@ -74,10 +107,6 @@ pub(crate) use tengri::{ }, }; -pub(crate) use ConnectName::*; -pub(crate) use ConnectScope::*; -pub(crate) use ConnectStatus::*; - /// Command-line entrypoint. #[cfg(feature = "cli")] pub fn main () -> Usually<()> { Config::watch(|config|{ @@ -428,39 +457,6 @@ impl> + AsMut>> HasDevices for T { } } -impl Device { - pub fn name (&self) -> &str { - match self { - Self::Sampler(sampler) => sampler.name.as_ref(), - _ => todo!(), - } - } - pub fn midi_ins (&self) -> &[MidiInput] { - match self { - //Self::Sampler(Sampler { midi_in, .. }) => &[midi_in], - _ => todo!() - } - } - pub fn midi_outs (&self) -> &[MidiOutput] { - match self { - Self::Sampler(_) => &[], - _ => todo!() - } - } - pub fn audio_ins (&self) -> &[AudioInput] { - match self { - Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(), - _ => todo!() - } - } - pub fn audio_outs (&self) -> &[AudioOutput] { - match self { - Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(), - _ => todo!() - } - } -} - //take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); @@ -604,32 +600,6 @@ def_command!(AppCommand: |app: App| { }, }); -def_command!(ClockCommand: |clock: Clock| { - SeekUsec { usec: f64 } => { - clock.playhead.update_from_usec(*usec); Ok(None) }, - SeekSample { sample: f64 } => { - clock.playhead.update_from_sample(*sample); Ok(None) }, - SeekPulse { pulse: f64 } => { - clock.playhead.update_from_pulse(*pulse); Ok(None) }, - SetBpm { bpm: f64 } => Ok(Some( - Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })), - SetQuant { quant: f64 } => Ok(Some( - Self::SetQuant { quant: clock.quant.set(*quant) })), - SetSync { sync: f64 } => Ok(Some( - Self::SetSync { sync: clock.sync.set(*sync) })), - - Play { position: Option } => { - clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ }, - Pause { position: Option } => { - clock.pause_at(*position)?; Ok(None) }, - - TogglePlayback { position: u32 } => Ok(if clock.is_rolling() { - clock.pause_at(Some(*position))?; None - } else { - clock.play_from(Some(*position))?; None - }), -}); - def_command!(DeviceCommand: |device: Device| {}); def_command!(ClipCommand: |clip: MidiClip| { @@ -941,282 +911,217 @@ def_command!(TrackCommand: |track: Track| { toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }), }); -pub(crate) use self::size::*; -mod size { - use crate::*; - /// Define a type alias for iterators of sized items (columns). - macro_rules! def_sizes_iter { - ($Type:ident => $($Item:ty),+) => { - pub trait $Type<'a> = - Iterator + Send + Sync + 'a; - } +/// Define a type alias for iterators of sized items (columns). +macro_rules! def_sizes_iter { + ($Type:ident => $($Item:ty),+) => { + pub trait $Type<'a> = + Iterator + Send + Sync + 'a; } - def_sizes_iter!(InputsSizes => MidiInput); - def_sizes_iter!(OutputsSizes => MidiOutput); - def_sizes_iter!(PortsSizes => Arc, [Connect]); - def_sizes_iter!(ScenesSizes => Scene); - def_sizes_iter!(TracksSizes => Track); +} +def_sizes_iter!(InputsSizes => MidiInput); +def_sizes_iter!(OutputsSizes => MidiOutput); +def_sizes_iter!(PortsSizes => Arc, [Connect]); +def_sizes_iter!(ScenesSizes => Scene); +def_sizes_iter!(TracksSizes => Track); + +/// ``` +/// let _ = tek::view_logo(); +/// ``` +pub fn view_logo () -> impl Draw { + wh_exact(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), south!{ + h_exact(1, ""), + h_exact(1, ""), + h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), + h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), + h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), + }))) } -pub use self::view::*; -mod view { - use crate::*; - - /// ``` - /// let _ = tek::view_logo(); - /// ``` - pub fn view_logo () -> impl Draw { - wh_exact(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), south!{ - h_exact(1, ""), - h_exact(1, ""), - h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), - h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), - h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), - }))) - } - - /// ``` - /// let x = std::sync::Arc::>::default(); - /// let _ = tek::view_transport(true, x.clone(), x.clone(), x.clone()); - /// let _ = tek::view_transport(false, x.clone(), x.clone(), x.clone()); - /// ``` - pub fn view_transport ( - play: bool, - bpm: Arc>, - beat: Arc>, - time: Arc>, - ) -> impl Draw { - let theme = ItemTheme::G[96]; - Tui::bg(Black, east!(above( - wh_full(origin_w(button_play_pause(play))), - wh_full(origin_e(east!( - field_h(theme, "BPM", bpm), - field_h(theme, "Beat", beat), - field_h(theme, "Time", time), - ))) +/// ``` +/// let x = std::sync::Arc::>::default(); +/// let _ = tek::view_transport(true, x.clone(), x.clone(), x.clone()); +/// let _ = tek::view_transport(false, x.clone(), x.clone(), x.clone()); +/// ``` +pub fn view_transport ( + play: bool, + bpm: Arc>, + beat: Arc>, + time: Arc>, +) -> impl Draw { + let theme = ItemTheme::G[96]; + Tui::bg(Black, east!(above( + wh_full(origin_w(button_play_pause(play))), + wh_full(origin_e(east!( + field_h(theme, "BPM", bpm), + field_h(theme, "Beat", beat), + field_h(theme, "Time", time), ))) - } + ))) +} - /// ``` - /// let x = std::sync::Arc::>::default(); - /// let _ = tek::view_status(None, x.clone(), x.clone(), x.clone()); - /// let _ = tek::view_status(Some("".into()), x.clone(), x.clone(), x.clone()); - /// ``` - pub fn view_status ( - sel: Option>, - sr: Arc>, - buf: Arc>, - lat: Arc>, - ) -> impl Draw { - let theme = ItemTheme::G[96]; - Tui::bg(Black, east!(above( - wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))), - wh_full(origin_e(east!( - field_h(theme, "SR", sr), - field_h(theme, "Buf", buf), - field_h(theme, "Lat", lat), - ))) +/// ``` +/// let x = std::sync::Arc::>::default(); +/// let _ = tek::view_status(None, x.clone(), x.clone(), x.clone()); +/// let _ = tek::view_status(Some("".into()), x.clone(), x.clone(), x.clone()); +/// ``` +pub fn view_status ( + sel: Option>, + sr: Arc>, + buf: Arc>, + lat: Arc>, +) -> impl Draw { + let theme = ItemTheme::G[96]; + Tui::bg(Black, east!(above( + wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))), + wh_full(origin_e(east!( + field_h(theme, "SR", sr), + field_h(theme, "Buf", buf), + field_h(theme, "Lat", lat), ))) - } + ))) +} - /// ``` - /// let _ = tek::button_play_pause(true); - /// ``` - pub fn button_play_pause (playing: bool) -> impl Draw { - let compact = true;//self.is_editing(); - Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - either(compact, - Thunk::new(move|to: &mut Tui|to.place(&w_exact(9, either(playing, - Tui::fg(Rgb(0, 255, 0), " PLAYING "), - Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - )), - Thunk::new(move|to: &mut Tui|to.place(&w_exact(5, either(playing, - Tui::fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - Tui::fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)))) - )) - ) - ) - } - - #[cfg(feature = "track")] pub fn view_track_row_section ( - _theme: ItemTheme, - button: impl Draw, - button_add: impl Draw, - content: impl Draw, - ) -> impl Draw { - west(h_full(w_exact(4, origin_nw(button_add))), - east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content)))) - } - - /// ``` - /// let bg = tengri::ratatui::style::Color::Red; - /// let fg = tengri::ratatui::style::Color::Green; - /// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); - /// ``` - pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw { - let left = Tui::fg_bg(bg, Reset, w_exact(1, y_repeat("▐"))); - let right = Tui::fg_bg(bg, Reset, w_exact(1, y_repeat("▌"))); - east(left, west(right, Tui::fg_bg(fg, bg, content))) - } - - /// ``` - /// let _ = tek::view_meter("", 0.0); - /// let _ = tek::view_meters(&[0.0, 0.0]); - /// ``` - pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { - south!( - field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)), - wh_exact(if value >= 0.0 { 13 } - else if value >= -1.0 { 12 } - else if value >= -2.0 { 11 } - else if value >= -3.0 { 10 } - else if value >= -4.0 { 9 } - else if value >= -6.0 { 8 } - else if value >= -9.0 { 7 } - else if value >= -12.0 { 6 } - else if value >= -15.0 { 5 } - else if value >= -20.0 { 4 } - else if value >= -25.0 { 3 } - else if value >= -30.0 { 2 } - else if value >= -40.0 { 1 } - else { 0 }, 1, Tui::bg(if value >= 0.0 { Red } - else if value >= -3.0 { Yellow } - else { Green }, ()))) - } - - pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { - let left = format!("L/{:>+9.3}", values[0]); - let right = format!("R/{:>+9.3}", values[1]); - south(left, right) - } - - pub fn draw_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { - when(sample.is_some(), Thunk::new(move|to: &mut Tui|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - to.place(&east!( - field_h(theme, "Name", format!("{:<10}", sample.name.clone())), - field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), - field_h(theme, "Start", format!("{:<8}", sample.start)), - field_h(theme, "End", format!("{:<8}", sample.end)), - field_h(theme, "Trans", "0"), - field_h(theme, "Gain", format!("{}", sample.gain)), +/// ``` +/// let _ = tek::button_play_pause(true); +/// ``` +pub fn button_play_pause (playing: bool) -> impl Draw { + let compact = true;//self.is_editing(); + Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + either(compact, + Thunk::new(move|to: &mut Tui|to.place(&w_exact(9, either(playing, + Tui::fg(Rgb(0, 255, 0), " PLAYING "), + Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + )), + Thunk::new(move|to: &mut Tui|to.place(&w_exact(5, either(playing, + Tui::fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + Tui::fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)))) )) - })) - } - - pub fn draw_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { - either(sample.is_some(), Thunk::new(move|to: &mut Tui|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - to.place(&w_exact(20, south!( - w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))), - w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))), - w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))), - w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))), - w_full(origin_w(field_h(theme, "Trans ", "0"))), - w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))), - ))) - }), Thunk::new(|to: &mut Tui|to.place(&Tui::fg(Red, south!( - Tui::bold(true, "× No sample."), - "[r] record", - "[Shift-F9] import", - ))))) - } - - pub fn draw_status (sample: Option<&Arc>>) -> impl Draw { - Tui::bold(true, Tui::fg(Tui::g(224), sample - .map(|sample|{ - let sample = sample.read().unwrap(); - format!("Sample {}-{}", sample.start, sample.end) - }) - .unwrap_or_else(||"No sample".to_string()))) - } - - pub fn view_track_header (theme: ItemTheme, content: impl Draw) -> impl Draw { - w_exact(12, Tui::bg(theme.darker.rgb, w_full(origin_e(content)))) - } - - pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) - -> impl Draw + use<'a, T> - { - let ins = ports.len() as u16; - let frame = Outer(true, Style::default().fg(Tui::g(96))); - let iter = move||ports.iter(); - let names = iter_south(1, iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); - let field = field_v(theme, title, names); - wh_exact(20, 1 + ins, frame.enclose(wh_exact(20, 1 + ins, field))) - } - - pub fn io_ports <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a - ) -> impl Draw + 'a { - iter(items, move|( - _index, name, connections, y, y2 - ): (usize, &'a Arc, &'a [Connect], usize, usize), _| - iter_south(y as u16, (y2-y) as u16, south( - h_full(Tui::bold(true, Tui::fg_bg(fg, bg, origin_w(east(&" 󰣲 ", name))))), - iter(||connections.iter(), move|connect: &'a Connect, index|iter_south(index as u16, 1, - h_full(origin_w(Tui::bold(false, Tui::fg_bg(fg, bg, - &connect.info))))))))) - } + ) + ) } -#[cfg(test)] mod test_view_meter { - use super::*; - use proptest::prelude::*; - proptest! { - - #[test] fn proptest_view_meter ( - label in "\\PC*", value in f32::MIN..f32::MAX - ) { - let _ = view_meter(&label, value); - } - - #[test] fn proptest_view_meters ( - value1 in f32::MIN..f32::MAX, - value2 in f32::MIN..f32::MAX - ) { - let _ = view_meters(&[value1, value2]); - } - } +#[cfg(feature = "track")] pub fn view_track_row_section ( + _theme: ItemTheme, + button: impl Draw, + button_add: impl Draw, + content: impl Draw, +) -> impl Draw { + west(h_full(w_exact(4, origin_nw(button_add))), + east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content)))) } -pub const DEFAULT_PPQ: f64 = 96.0; +/// ``` +/// let bg = tengri::ratatui::style::Color::Red; +/// let fg = tengri::ratatui::style::Color::Green; +/// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); +/// ``` +pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw { + let left = Tui::fg_bg(bg, Reset, w_exact(1, y_repeat("▐"))); + let right = Tui::fg_bg(bg, Reset, w_exact(1, y_repeat("▌"))); + east(left, west(right, Tui::fg_bg(fg, bg, content))) +} -/// FIXME: remove this and use PPQ from timebase everywhere: -pub const PPQ: usize = 96; +/// ``` +/// let _ = tek::view_meter("", 0.0); +/// let _ = tek::view_meters(&[0.0, 0.0]); +/// ``` +pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { + south!( + field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)), + wh_exact(if value >= 0.0 { 13 } + else if value >= -1.0 { 12 } + else if value >= -2.0 { 11 } + else if value >= -3.0 { 10 } + else if value >= -4.0 { 9 } + else if value >= -6.0 { 8 } + else if value >= -9.0 { 7 } + else if value >= -12.0 { 6 } + else if value >= -15.0 { 5 } + else if value >= -20.0 { 4 } + else if value >= -25.0 { 3 } + else if value >= -30.0 { 2 } + else if value >= -40.0 { 1 } + else { 0 }, 1, Tui::bg(if value >= 0.0 { Red } + else if value >= -3.0 { Yellow } + else { Green }, ()))) +} -/// (pulses, name), assuming 96 PPQ -pub const NOTE_DURATIONS: [(usize, &str);26] = [ - (1, "1/384"), (2, "1/192"), - (3, "1/128"), (4, "1/96"), - (6, "1/64"), (8, "1/48"), - (12, "1/32"), (16, "1/24"), - (24, "1/16"), (32, "1/12"), - (48, "1/8"), (64, "1/6"), - (96, "1/4"), (128, "1/3"), - (192, "1/2"), (256, "2/3"), - (384, "1/1"), (512, "4/3"), - (576, "3/2"), (768, "2/1"), - (1152, "3/1"), (1536, "4/1"), - (2304, "6/1"), (3072, "8/1"), - (3456, "9/1"), (6144, "16/1"), -]; +pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { + let left = format!("L/{:>+9.3}", values[0]); + let right = format!("R/{:>+9.3}", values[1]); + south(left, right) +} -pub const NOTE_NAMES: [&str; 128] = [ - "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", - "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", - "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", - "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", - "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", - "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", - "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6", - "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", - "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", - "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", - "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", -]; +pub fn draw_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { + when(sample.is_some(), Thunk::new(move|to: &mut Tui|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + to.place(&east!( + field_h(theme, "Name", format!("{:<10}", sample.name.clone())), + field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), + field_h(theme, "Start", format!("{:<8}", sample.start)), + field_h(theme, "End", format!("{:<8}", sample.end)), + field_h(theme, "Trans", "0"), + field_h(theme, "Gain", format!("{}", sample.gain)), + )) + })) +} + +pub fn draw_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { + either(sample.is_some(), Thunk::new(move|to: &mut Tui|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + to.place(&w_exact(20, south!( + w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))), + w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))), + w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))), + w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))), + w_full(origin_w(field_h(theme, "Trans ", "0"))), + w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))), + ))) + }), Thunk::new(|to: &mut Tui|to.place(&Tui::fg(Red, south!( + Tui::bold(true, "× No sample."), + "[r] record", + "[Shift-F9] import", + ))))) +} + +pub fn draw_status (sample: Option<&Arc>>) -> impl Draw { + Tui::bold(true, Tui::fg(Tui::g(224), sample + .map(|sample|{ + let sample = sample.read().unwrap(); + format!("Sample {}-{}", sample.start, sample.end) + }) + .unwrap_or_else(||"No sample".to_string()))) +} + +pub fn view_track_header (theme: ItemTheme, content: impl Draw) -> impl Draw { + w_exact(12, Tui::bg(theme.darker.rgb, w_full(origin_e(content)))) +} + +pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) + -> impl Draw + use<'a, T> +{ + let ins = ports.len() as u16; + let frame = Outer(true, Style::default().fg(Tui::g(96))); + let iter = move||ports.iter(); + let names = iter_south(1, iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); + let field = field_v(theme, title, names); + wh_exact(20, 1 + ins, frame.enclose(wh_exact(20, 1 + ins, field))) +} + +pub fn io_ports <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a +) -> impl Draw + 'a { + iter(items, move|( + _index, name, connections, y, y2 + ): (usize, &'a Arc, &'a [Connect], usize, usize), _| + iter_south(y as u16, (y2-y) as u16, south( + h_full(Tui::bold(true, Tui::fg_bg(fg, bg, origin_w(east(&" 󰣲 ", name))))), + iter(||connections.iter(), move|connect: &'a Connect, index|iter_south(index as u16, 1, + h_full(origin_w(Tui::bold(false, Tui::fg_bg(fg, bg, + &connect.info))))))))) +} /// CLI banner. pub(crate) const HEADER: &'static str = r#" @@ -1501,308 +1406,776 @@ pub trait HasWidth { Options, } -/// A device that can be plugged into the chain. -/// -/// ``` -/// let device = tek::Device::default(); -/// ``` -#[derive(Debug, Default)] pub enum Device { - #[default] - Bypass, - Mute, - #[cfg(feature = "sampler")] - Sampler(Sampler), - #[cfg(feature = "lv2")] // TODO - Lv2(Lv2), - #[cfg(feature = "vst2")] // TODO - Vst2, - #[cfg(feature = "vst3")] // TODO - Vst3, - #[cfg(feature = "clap")] // TODO - Clap, - #[cfg(feature = "sf2")] // TODO - Sf2, +/// Command-line configuration. +#[cfg(feature = "cli")] impl Cli { + pub fn run (&self) -> Usually<()> { + if let Action::Version = self.action { + return Ok(tek_show_version()) + } + + let mut config = Config::new(None); + config.init()?; + + if let Action::Config = self.action { + tek_print_config(&config); + } else if let Action::List = self.action { + todo!("list sessions") + } else if let Action::Resume = self.action { + todo!("resume session") + } else if let Action::New { + name, bpm, tracks, scenes, sync_lead, sync_follow, + midi_from, midi_from_re, midi_to, midi_to_re, + left_from, right_from, left_to, right_to, .. + } = &self.action { + + // Connect to JACK + let name = name.as_ref().map_or("tek", |x|x.as_str()); + let jack = Jack::new(&name)?; + + // TODO: Collect audio IO: + let empty = &[] as &[&str]; + let left_froms = Connect::collect(&left_from, empty, empty); + let left_tos = Connect::collect(&left_to, empty, empty); + let right_froms = Connect::collect(&right_from, empty, empty); + let right_tos = Connect::collect(&right_to, empty, empty); + let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; + let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; + + // Create initial project: + let clock = Clock::new(&jack, *bpm)?; + let mut project = Arrangement::new( + &jack, + None, + clock, + vec![], + vec![], + Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() + .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) + .collect::, _>>()?, + Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() + .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) + .collect::, _>>()? + ); + project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; + project.scenes_add(scenes.unwrap_or(0))?; + + if matches!(self.action, Action::Status) { + // Show status and exit + tek_print_status(&project); + return Ok(()) + } + + // Initialize the app state + let app = tek(&jack, project, config, ":menu"); + if matches!(self.action, Action::Headless) { + // TODO: Headless mode (daemon + client over IPC, then over network...) + println!("todo headless"); + return Ok(()) + } + + // Run the [Tui] and [Jack] threads with the [App] state. + Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ + + // Between jack init and app's first cycle: + + jack.sync_lead(*sync_lead, |mut state|{ + let clock = app.clock(); + clock.playhead.update_from_sample(state.position.frame() as f64); + state.position.bbt = Some(clock.bbt()); + state.position + })?; + + jack.sync_follow(*sync_follow)?; + + // FIXME: They don't work properly. + + Ok(app) + + })?)?; + } + Ok(()) + } } -/// Some sort of wrapper? -pub struct DeviceAudio<'a>(pub &'a mut Device); - - /// Command-line configuration. - #[cfg(feature = "cli")] impl Cli { - pub fn run (&self) -> Usually<()> { - if let Action::Version = self.action { - return Ok(tek_show_version()) - } - - let mut config = Config::new(None); - config.init()?; - - if let Action::Config = self.action { - tek_print_config(&config); - } else if let Action::List = self.action { - todo!("list sessions") - } else if let Action::Resume = self.action { - todo!("resume session") - } else if let Action::New { - name, bpm, tracks, scenes, sync_lead, sync_follow, - midi_from, midi_from_re, midi_to, midi_to_re, - left_from, right_from, left_to, right_to, .. - } = &self.action { - - // Connect to JACK - let name = name.as_ref().map_or("tek", |x|x.as_str()); - let jack = Jack::new(&name)?; - - // TODO: Collect audio IO: - let empty = &[] as &[&str]; - let left_froms = Connect::collect(&left_from, empty, empty); - let left_tos = Connect::collect(&left_to, empty, empty); - let right_froms = Connect::collect(&right_from, empty, empty); - let right_tos = Connect::collect(&right_to, empty, empty); - let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; - let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; - - // Create initial project: - let clock = Clock::new(&jack, *bpm)?; - let mut project = Arrangement::new( - &jack, - None, - clock, - vec![], - vec![], - Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() - .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) - .collect::, _>>()?, - Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() - .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) - .collect::, _>>()? - ); - project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; - project.scenes_add(scenes.unwrap_or(0))?; - - if matches!(self.action, Action::Status) { - // Show status and exit - tek_print_status(&project); - return Ok(()) - } - - // Initialize the app state - let app = tek(&jack, project, config, ":menu"); - if matches!(self.action, Action::Headless) { - // TODO: Headless mode (daemon + client over IPC, then over network...) - println!("todo headless"); - return Ok(()) - } - - // Run the [Tui] and [Jack] threads with the [App] state. - Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ - - // Between jack init and app's first cycle: - - jack.sync_lead(*sync_lead, |mut state|{ - let clock = app.clock(); - clock.playhead.update_from_sample(state.position.frame() as f64); - state.position.bbt = Some(clock.bbt()); - state.position - })?; - - jack.sync_follow(*sync_follow)?; - - // FIXME: They don't work properly. - - Ok(app) - - })?)?; +impl Config { + const CONFIG_DIR: &'static str = "tek"; + const CONFIG_SUB: &'static str = "v0"; + const CONFIG: &'static str = "tek.edn"; + const DEFAULTS: &'static str = include_str!("./tek.edn"); + /// Create a new app configuration from a set of XDG base directories, + pub fn new (dirs: Option) -> Self { + let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); + let dirs = dirs.unwrap_or_else(default); + Self { dirs, ..Default::default() } + } + /// Write initial contents of configuration. + pub fn init (&mut self) -> Usually<()> { + self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ + cfgs.add(&dsl)?; + Ok(()) + })?; + Ok(()) + } + /// Write initial contents of a configuration file. + pub fn init_one ( + &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> + ) -> Usually<()> { + if self.dirs.find_config_file(path).is_none() { + //println!("Creating {path:?}"); + std::fs::write(self.dirs.place_config_file(path)?, defaults)?; + } + Ok(if let Some(path) = self.dirs.find_config_file(path) { + //println!("Loading {path:?}"); + let src = std::fs::read_to_string(&path)?; + src.as_str().each(move|item|each(self, item))?; + } else { + return Err(format!("{path}: not found").into()) + }) + } + /// Add statements to configuration from [Dsl] source. + pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { + dsl.each(|item|self.add_one(item))?; + Ok(self) + } + fn add_one (&self, item: impl Language) -> Usually<()> { + if let Some(expr) = item.expr()? { + let head = expr.head()?; + let tail = expr.tail()?; + let name = tail.head()?; + let body = tail.tail()?; + //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); + match head { + Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, + Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, + Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, + _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) } Ok(()) + } else { + return Err(format!("Config::load: expected expr, got: {item:?}").into()) } } + pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { + self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() + } +} - impl Config { - const CONFIG_DIR: &'static str = "tek"; - const CONFIG_SUB: &'static str = "v0"; - const CONFIG: &'static str = "tek.edn"; - const DEFAULTS: &'static str = include_str!("./tek.edn"); - /// Create a new app configuration from a set of XDG base directories, - pub fn new (dirs: Option) -> Self { - let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); - let dirs = dirs.unwrap_or_else(default); - Self { dirs, ..Default::default() } +impl Mode> { + /// Add a definition to the mode. + /// + /// Supported definitions: + /// + /// - (name ...) -> name + /// - (info ...) -> description + /// - (keys ...) -> key bindings + /// - (mode ...) -> submode + /// - ... -> view + /// + /// ``` + /// let mut mode: tek::Mode> = Default::default(); + /// mode.add("(name hello)").unwrap(); + /// ``` + pub fn add (&mut self, dsl: impl Language) -> Usually<()> { + Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { + //println!("Mode::add: {head} {:?}", expr.tail()); + let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + match head { + "name" => self.add_name(tail)?, + "info" => self.add_info(tail)?, + "keys" => self.add_keys(tail)?, + "mode" => self.add_mode(tail)?, + _ => self.add_view(tail)?, + }; + } else if let Ok(Some(word)) = dsl.word() { + self.add_view(word); + } else { + return Err(format!("Mode::add: unexpected: {dsl:?}").into()); + }) + + //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) + //.word(|word|self.add_view(word)) + //.expr(|expr|expr.head(|head|{ + ////println!("Mode::add: {head} {:?}", expr.tail()); + //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + //match head { + //"name" => self.add_name(tail), + //"info" => self.add_info(tail), + //"keys" => self.add_keys(tail)?, + //"mode" => self.add_mode(tail)?, + //_ => self.add_view(tail), + //}; + //})) + } + + fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.name.push(src.into()))) + } + fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.info.push(src.into()))) + } + fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.view.push(src.into()))) + } + fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) + } + fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(if let Some(id) = dsl.head()? { + load_mode(&self.modes, &id, &dsl.tail())?; + } else { + return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); + })) + } +} + +impl Bind { + /// Create a new event map + pub fn new () -> Self { + Default::default() + } + /// Add a binding to an owned event map. + pub fn def (mut self, event: E, binding: Binding) -> Self { + self.add(event, binding); + self + } + /// Add a binding to an event map. + pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { + if !self.0.contains_key(&event) { + self.0.insert(event.clone(), Default::default()); } - /// Write initial contents of configuration. - pub fn init (&mut self) -> Usually<()> { - self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ - cfgs.add(&dsl)?; - Ok(()) - })?; + self.0.get_mut(&event).unwrap().push(binding); + self + } + /// Return the binding(s) that correspond to an event. + pub fn query (&self, event: &E) -> Option<&[Binding]> { + self.0.get(event).map(|x|x.as_slice()) + } + /// Return the first binding that corresponds to an event, considering conditions. + pub fn dispatch (&self, event: &E) -> Option<&Binding> { + self.query(event) + .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) + .flatten() + } +} + +impl Bind> { + pub fn load (lang: &impl Language) -> Usually { + let mut map = Bind::new(); + lang.each(|item|if item.expr().head() == Ok(Some("see")) { + // TODO Ok(()) - } - /// Write initial contents of a configuration file. - pub fn init_one ( - &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> - ) -> Usually<()> { - if self.dirs.find_config_file(path).is_none() { - //println!("Creating {path:?}"); - std::fs::write(self.dirs.place_config_file(path)?, defaults)?; - } - Ok(if let Some(path) = self.dirs.find_config_file(path) { - //println!("Loading {path:?}"); - let src = std::fs::read_to_string(&path)?; - src.as_str().each(move|item|each(self, item))?; - } else { - return Err(format!("{path}: not found").into()) - }) - } - /// Add statements to configuration from [Dsl] source. - pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { - dsl.each(|item|self.add_one(item))?; - Ok(self) - } - fn add_one (&self, item: impl Language) -> Usually<()> { - if let Some(expr) = item.expr()? { - let head = expr.head()?; - let tail = expr.tail()?; - let name = tail.head()?; - let body = tail.tail()?; - //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); - match head { - Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, - Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, - Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, - _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) - } + } else if let Ok(Some(_word)) = item.expr().head().word() { + if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { + map.add(key, Binding { + commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), + condition: None, + description: None, + source: None + }); Ok(()) - } else { - return Err(format!("Config::load: expected expr, got: {item:?}").into()) - } - } - pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { - self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() - } - } - - impl Mode> { - /// Add a definition to the mode. - /// - /// Supported definitions: - /// - /// - (name ...) -> name - /// - (info ...) -> description - /// - (keys ...) -> key bindings - /// - (mode ...) -> submode - /// - ... -> view - /// - /// ``` - /// let mut mode: tek::Mode> = Default::default(); - /// mode.add("(name hello)").unwrap(); - /// ``` - pub fn add (&mut self, dsl: impl Language) -> Usually<()> { - Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { - //println!("Mode::add: {head} {:?}", expr.tail()); - let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - match head { - "name" => self.add_name(tail)?, - "info" => self.add_info(tail)?, - "keys" => self.add_keys(tail)?, - "mode" => self.add_mode(tail)?, - _ => self.add_view(tail)?, - }; - } else if let Ok(Some(word)) = dsl.word() { - self.add_view(word); - } else { - return Err(format!("Mode::add: unexpected: {dsl:?}").into()); - }) - - //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) - //.word(|word|self.add_view(word)) - //.expr(|expr|expr.head(|head|{ - ////println!("Mode::add: {head} {:?}", expr.tail()); - //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - //match head { - //"name" => self.add_name(tail), - //"info" => self.add_info(tail), - //"keys" => self.add_keys(tail)?, - //"mode" => self.add_mode(tail)?, - //_ => self.add_view(tail), - //}; - //})) - } - - fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.name.push(src.into()))) - } - fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.info.push(src.into()))) - } - fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.view.push(src.into()))) - } - fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) - } - fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(if let Some(id) = dsl.head()? { - load_mode(&self.modes, &id, &dsl.tail())?; - } else { - return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); - })) - } - } - - impl Bind { - /// Create a new event map - pub fn new () -> Self { - Default::default() - } - /// Add a binding to an owned event map. - pub fn def (mut self, event: E, binding: Binding) -> Self { - self.add(event, binding); - self - } - /// Add a binding to an event map. - pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { - if !self.0.contains_key(&event) { - self.0.insert(event.clone(), Default::default()); - } - self.0.get_mut(&event).unwrap().push(binding); - self - } - /// Return the binding(s) that correspond to an event. - pub fn query (&self, event: &E) -> Option<&[Binding]> { - self.0.get(event).map(|x|x.as_slice()) - } - /// Return the first binding that corresponds to an event, considering conditions. - pub fn dispatch (&self, event: &E) -> Option<&Binding> { - self.query(event) - .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) - .flatten() - } - } - - impl Bind> { - pub fn load (lang: &impl Language) -> Usually { - let mut map = Bind::new(); - lang.each(|item|if item.expr().head() == Ok(Some("see")) { + } else if Some(":char") == item.expr()?.head()? { // TODO - Ok(()) - } else if let Ok(Some(_word)) = item.expr().head().word() { - if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { - map.add(key, Binding { - commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), - condition: None, - description: None, - source: None - }); - Ok(()) - } else if Some(":char") == item.expr()?.head()? { - // TODO - return Ok(()) - } else { - return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) - } + return Ok(()) } else { - return Err(format!("Config::load_bind: unexpected: {item:?}").into()) - })?; - Ok(map) + return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) + } + } else { + return Err(format!("Config::load_bind: unexpected: {item:?}").into()) + })?; + Ok(map) + } +} + +impl Dialog { + /// ``` + /// let _ = tek::Dialog::welcome(); + /// ``` + pub fn welcome () -> Self { + Self::Menu(1, MenuItems([ + MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), + MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ + app.dialog = Dialog::None; + app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); + })))), + MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), + ].into())) + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().menu_selected(); + /// ``` + pub fn menu_selected (&self) -> Option { + if let Self::Menu(selected, _) = self { Some(*selected) } else { None } + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().menu_next(); + /// ``` + pub fn menu_next (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), + _ => Self::None } } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().menu_prev(); + /// ``` + pub fn menu_prev (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().device_kind(); + /// ``` + pub fn device_kind (&self) -> Option { + if let Self::Device(index) = self { Some(*index) } else { None } + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().device_kind_next(); + /// ``` + pub fn device_kind_next (&self) -> Option { + self.device_kind().map(|index|(index + 1) % device_kinds().len()) + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().device_kind_prev(); + /// ``` + pub fn device_kind_prev (&self) -> Option { + self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) + } + /// FIXME: implement + pub fn message (&self) -> Option<&str> { todo!() } + /// FIXME: implement + pub fn browser (&self) -> Option<&Arc> { todo!() } + /// FIXME: implement + pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } +} +use crate::*; +impl_has!(Clock: |self: App|self.project.clock); +impl_has!(Vec: |self: App|self.project.midi_ins); +impl_has!(Vec: |self: App|self.project.midi_outs); +impl_has!(Dialog: |self: App|self.dialog); +impl_has!(Jack<'static>: |self: App|self.jack); +impl_has!(Measure: |self: App|self.size); +impl_has!(Pool: |self: App|self.pool); +impl_has!(Selection: |self: App|self.project.selection); +impl_as_ref!(Vec: |self: App|self.project.as_ref()); +impl_as_mut!(Vec: |self: App|self.project.as_mut()); +impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); +impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); +impl_has_clips!( |self: App|self.pool.clips); +impl_audio!(App: tek_jack_process, tek_jack_event); +impl_handle!(TuiIn: |self: App, input|{ + let commands = collect_commands(self, input)?; + let history = execute_commands(self, commands)?; + self.history.extend(history.into_iter()); + Ok(None) +}); + +impl Draw for App { + fn draw (self, to: &mut Tui) -> Usually> { + if let Some(e) = self.error.read().unwrap().as_ref() { + to.show(to.area(), e); + } + for (index, dsl) in self.mode.view.iter().enumerate() { + if let Err(e) = self.understand(to, dsl) { + *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); + break; + } + } + } +} + +impl<'a> Namespace<'a, AppCommand> for App { + symbols!('a |app| -> AppCommand { + "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, + "x/dec" => AppCommand::Dec { axis: ControlAxis::X }, + "y/inc" => AppCommand::Inc { axis: ControlAxis::Y }, + "y/dec" => AppCommand::Dec { axis: ControlAxis::Y }, + "confirm" => AppCommand::Confirm, + "cancel" => AppCommand::Cancel, + }); +} + +impl Understand for App { + fn understand_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { + app_understand_expr(self, to, lang) + } + fn understand_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { + app_understand_word(self, to, lang) + } +} + +fn app_understand_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> { + if evaluate_output_expression(state, to, lang)? + || evaluate_output_expression_tui(state, to, lang)? { + Ok(()) + } else { + Err(format!("App::understand_expr: unexpected: {lang:?}").into()) + } +} + +fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> { + let mut frags = dsl.src()?.unwrap().split("/"); + match frags.next() { + Some(":logo") => to.place(&view_logo()), + Some(":status") => to.place(&h_exact(1, "TODO: Status Bar")), + Some(":meters") => match frags.next() { + Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters")))), + Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters")))), + _ => panic!() + }, + Some(":tracks") => match frags.next() { + None => to.place(&"TODO tracks"), + Some("names") => to.place(&state.project.view_track_names(state.color.clone())),//Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), + Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs")))), + Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices")))), + Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs")))), + _ => panic!() + }, + Some(":scenes") => match frags.next() { + None => to.place(&"TODO scenes"), + Some(":scenes/names") => to.place(&"TODO Scene Names"), + _ => panic!() + }, + Some(":editor") => to.place(&"TODO Editor"), + Some(":dialog") => match frags.next() { + Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &state.dialog { + let items = items.clone(); + let selected = selected; + Some(wh_full(Thunk::new(move|to: &mut Tui|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + to.place(&y_push((2 * index) as u16, + Tui::fg_bg( + if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, + if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, + h_exact(2, origin_n(w_full(item))) + ))); + } + }))) + } else { + None + }), + _ => unimplemented!("App::understand_word: {dsl:?} ({frags:?})"), + }, + Some(":templates") => to.place(&{ + let modes = state.config.modes.clone(); + let height = (modes.read().unwrap().len() * 2) as u16; + h_exact(height, w_min(30, Thunk::new(move |to: &mut Tui|{ + for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { + let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; + let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); + let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); + let fg1 = Rgb(224, 192, 128); + let fg2 = Rgb(224, 128, 32); + let field_name = w_full(origin_w(Tui::fg(fg1, name))); + let field_id = w_full(origin_e(Tui::fg(fg2, id))); + let field_info = w_full(origin_w(info)); + to.place(&y_push((2 * index) as u16, + h_exact(2, w_full(Tui::bg(bg, south( + above(field_name, field_id), field_info)))))); + } + }))) + }), + Some(":sessions") => to.place(&h_exact(6, w_min(30, Thunk::new(|to: &mut Tui|{ + let fg = Rgb(224, 192, 128); + for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { + let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; + to.place(&y_push((2 * index) as u16, + &h_exact(2, w_full(Tui::bg(bg, origin_w(Tui::fg(fg, name))))))); + } + })))), + Some(":browse/title") => to.place(&w_full(origin_w(field_v(ItemColor::default(), + match state.dialog.browser_target().unwrap() { + BrowseTarget::SaveProject => "Save project:", + BrowseTarget::LoadProject => "Load project:", + BrowseTarget::ImportSample(_) => "Import sample:", + BrowseTarget::ExportSample(_) => "Export sample:", + BrowseTarget::ImportClip(_) => "Import clip:", + BrowseTarget::ExportClip(_) => "Export clip:", + }, w_shrink(3, h_exact(1, Tui::fg(Tui::g(96), x_repeat("🭻")))))))), + Some(":device") => { + let selected = state.dialog.device_kind().unwrap(); + to.place(&south(Tui::bold(true, "Add device"), iter_south(1, + move||device_kinds().iter(), + move|_label: &&'static str, i|{ + let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let lb = if i == selected { "[ " } else { " " }; + let rb = if i == selected { " ]" } else { " " }; + w_full(Tui::bg(bg, east(lb, west(rb, "FIXME device name")))) }))) + }, + Some(":debug") => to.place(&h_exact(1, format!("[{:?}]", to.area()))), + Some(_) => { + let views = state.config.views.read().unwrap(); + if let Some(dsl) = views.get(dsl.src()?.unwrap()) { + let dsl = dsl.clone(); + std::mem::drop(views); + state.understand(to, &dsl)? + } else { + unimplemented!("{dsl:?}"); + } + }, + _ => unreachable!() + } + Ok(()) +} + +impl App { + /// Update memoized render of clock values. + /// ``` + /// tek::App::default().update_clock(); + /// ``` + pub fn update_clock (&self) { + ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) + } + + /// Set modal dialog. + /// + /// ``` + /// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome()); + /// ``` + pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { + std::mem::swap(&mut self.dialog, &mut dialog); + dialog + } + + /// FIXME: generalize. Set picked device in device pick dialog. + /// + /// ``` + /// tek::App::default().device_pick(0); + /// ``` + pub fn device_pick (&mut self, index: usize) { + self.dialog = Dialog::Device(index); + } + + /// FIXME: generalize. Add device to current track. + pub fn add_device (&mut self, index: usize) -> Usually<()> { + match index { + 0 => { + let name = self.jack.with_client(|c|c.name().to_string()); + let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); + let track = self.track().expect("no active track"); + let port = format!("{}/Sampler", &track.name); + let connect = Connect::exact(format!("{name}:{midi}")); + let sampler = if let Ok(sampler) = Sampler::new( + &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] + ) { + self.dialog = Dialog::None; + Device::Sampler(sampler) + } else { + self.dialog = Dialog::Message("Failed to add device.".into()); + return Err("failed to add device".into()) + }; + let track = self.track_mut().expect("no active track"); + track.devices.push(sampler); + Ok(()) + }, + 1 => { + todo!(); + //Ok(()) + }, + _ => unreachable!(), + } + } + + /// Return reference to content browser if open. + /// + /// ``` + /// assert_eq!(tek::App::default().browser(), None); + /// ``` + pub fn browser (&self) -> Option<&Browse> { + if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + } + + /// Is a MIDI editor currently focused? + /// + /// ``` + /// tek::App::default().editor_focused(); + /// ``` + pub fn editor_focused (&self) -> bool { + false + } + + /// Toggle MIDI editor. + /// + /// ``` + /// tek::App::default().toggle_editor(None); + /// ``` + pub fn toggle_editor (&mut self, value: Option) { + //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); + let value = value.unwrap_or_else(||!self.editor().is_some()); + if value { + // Create new clip in pool when entering empty cell + if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && slot.is_none() + && let Some(track) = self.project.tracks.get_mut(track) + { + let (_index, clip) = self.pool.add_new_clip(); + // autocolor: new clip colors from scene and track color + let color = track.color.base.mix(scene.color.base, 0.5); + clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); + if let Some(editor) = &mut self.project.editor { + editor.set_clip(Some(&clip)); + } + *slot = Some(clip.clone()); + //Some(clip) + } else { + //None + } + } else if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && let Some(clip) = slot.as_mut() + { + // Remove clip from arrangement when exiting empty clip editor + let mut swapped = None; + if clip.read().unwrap().count_midi_messages() == 0 { + std::mem::swap(&mut swapped, slot); + } + if let Some(clip) = swapped { + self.pool.delete_clip(&clip.read().unwrap()); + } + } + } +} + +impl +AsMut> HasClock for T {} +impl +AsMut> HasSelection for T {} +impl +AsMut> HasSequencer for T {} +impl >+AsMut>> HasScenes for T {} +impl >+AsMut>> HasTracks for T {} +impl +AsMutOpt> HasEditor for T {} +impl +AsMutOpt+Send+Sync> HasScene for T {} +impl +AsMutOpt+Send+Sync> HasTrack for T {} +impl MidiPoint for T {} +impl > TracksView for T {} +impl MidiRange for T {} +impl ClipsView for T {} + +/// Default is always empty map regardless if `E` and `C` implement [Default]. +impl Default for Bind { + fn default () -> Self { Self(Default::default()) } +} +impl Default for Binding { + fn default () -> Self { + Self { + commands: Default::default(), + condition: Default::default(), + description: Default::default(), + source: Default::default(), + } + } +} + +impl_default!(AppCommand: Self::Nop); +impl_default!(MenuItem: Self("".into(), Arc::new(Box::new(|_|Ok(()))))); +impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ)); + +impl Gettable for AtomicBool { fn get (&self) -> bool { self.load(Relaxed) } } +impl InteriorMutable for AtomicBool { fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) } } +impl Gettable for AtomicUsize { fn get (&self) -> usize { self.load(Relaxed) } } +impl InteriorMutable for AtomicUsize { fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) } } + +impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } +impl AsRef> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } } +impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.size_inner } } +impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } + +#[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock); + +impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); +impl_debug!(Condition |self, w| { write!(w, "*") }); + +primitive!(u8: try_to_u8); +primitive!(u16: try_to_u16); +primitive!(usize: try_to_usize); +primitive!(isize: try_to_isize); +namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); +namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); +namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| { + ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), + ":h/sample-detail" => 6.max(app.measure_height() as u16 * 3 / 9), }; }); +namespace!(App: isize { literal = |dsl|try_to_isize(dsl); }); +namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| { + ":scene-count" => app.scenes().len(), + ":track-count" => app.tracks().len(), + ":device-kind" => app.dialog.device_kind().unwrap_or(0), + ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), + ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), }; }); +namespace!(App: bool { symbol = |app| { // Provide boolean values. + ":mode/editor" => app.project.editor.is_some(), + ":focused/dialog" => !matches!(app.dialog, Dialog::None), + ":focused/message" => matches!(app.dialog, Dialog::Message(..)), + ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), + ":focused/browser" => app.dialog.browser().is_some(), + ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), + ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), + ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), + ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), + ":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}), + ":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)), + ":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)), + ":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix), +}; }); +namespace!(App: ItemTheme {}); // TODO: provide colors here +namespace!(App: Dialog { symbol = |app| { + ":dialog/none" => Dialog::None, + ":dialog/options" => Dialog::Options, + ":dialog/device" => Dialog::Device(0), + ":dialog/device/prev" => Dialog::Device(0), + ":dialog/device/next" => Dialog::Device(0), + ":dialog/help" => Dialog::Help(0), + ":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, + Browse::new(None).unwrap().into()), + ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, + Browse::new(None).unwrap().into()), + ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), + Browse::new(None).unwrap().into()), +}; }); +namespace!(App: Selection { symbol = |app| { + ":select/scene" => app.selection().select_scene(app.tracks().len()), + ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), + ":select/scene/prev" => app.selection().select_scene_prev(), + ":select/track" => app.selection().select_track(app.tracks().len()), + ":select/track/next" => app.selection().select_track_next(app.tracks().len()), + ":select/track/prev" => app.selection().select_track_prev(), +}; }); +namespace!(App: Color { + symbol = |app| { + ":color/bg" => Color::Rgb(28, 32, 36), + }; + expression = |app| { + "g" (n: u8) => Color::Rgb(n, n, n), + "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), + }; +}); +namespace!(App: Option { symbol = |app| { + ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) +}; }); +namespace!(App: Option { symbol = |app| { + ":selected/scene" => app.selection().scene(), + ":selected/track" => app.selection().track(), +}; }); +namespace!(App: Option>> { + symbol = |app| { + ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { + app.scenes()[*scene].clips[*track].clone() + } else { + None + } + }; +}); diff --git a/src/tek_impls.rs b/src/tek_impls.rs deleted file mode 100644 index 3e245cfe..00000000 --- a/src/tek_impls.rs +++ /dev/null @@ -1,3586 +0,0 @@ -use crate::*; -use std::fmt::Write; - -/// Implement an arithmetic operation for a unit of time -#[macro_export] macro_rules! impl_op { - ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { - let $a = self.get(); let $b = other.get(); Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { - let $a = self.get(); let $b = other as f64; Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { - let $a = self.get(); let $b = other; Self($impl.into()) - } - } - } -} - -/// Define and implement a unit of time -#[macro_export] macro_rules! impl_time_unit { - ($T:ident) => { - impl Gettable for $T { - fn get (&self) -> f64 { self.0.load(Relaxed) } - } - impl InteriorMutable for $T { - fn set (&self, value: f64) -> f64 { - let old = self.get(); - self.0.store(value, Relaxed); - old - } - } - impl TimeUnit for $T {} - impl_op!($T, Add, add, |a, b|{a + b}); - impl_op!($T, Sub, sub, |a, b|{a - b}); - impl_op!($T, Mul, mul, |a, b|{a * b}); - impl_op!($T, Div, div, |a, b|{a / b}); - impl_op!($T, Rem, rem, |a, b|{a % b}); - impl From for $T { fn from (value: f64) -> Self { Self(value.into()) } } - impl From for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } } - impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } } - impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } } - impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } } - impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } } - impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } - } -} - -#[macro_export] macro_rules! impl_has_clips { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { - fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { - $cb.read().unwrap() - } - fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { - $cb.write().unwrap() - } - } - } -} - -impl +AsMut> HasClock for T {} -impl +AsMut> HasSelection for T {} -impl +AsMut> HasSequencer for T {} -impl >+AsMut>> HasScenes for T {} -impl >+AsMut>> HasTracks for T {} -impl +AsMutOpt> HasEditor for T {} -impl +AsMutOpt+Send+Sync> HasScene for T {} -impl +AsMutOpt+Send+Sync> HasTrack for T {} -impl MidiPoint for T {} -impl > TracksView for T {} -impl MidiRange for T {} -impl ClipsView for T {} - -mod app { - use crate::*; - impl_has!(Clock: |self: App|self.project.clock); - impl_has!(Vec: |self: App|self.project.midi_ins); - impl_has!(Vec: |self: App|self.project.midi_outs); - impl_has!(Dialog: |self: App|self.dialog); - impl_has!(Jack<'static>: |self: App|self.jack); - impl_has!(Measure: |self: App|self.size); - impl_has!(Pool: |self: App|self.pool); - impl_has!(Selection: |self: App|self.project.selection); - impl_as_ref!(Vec: |self: App|self.project.as_ref()); - impl_as_mut!(Vec: |self: App|self.project.as_mut()); - impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); - impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); - impl_has_clips!( |self: App|self.pool.clips); - impl_audio!(App: tek_jack_process, tek_jack_event); - impl_handle!(TuiIn: |self: App, input|{ - let commands = collect_commands(self, input)?; - let history = execute_commands(self, commands)?; - self.history.extend(history.into_iter()); - Ok(None) - }); - - impl Draw for App { - fn draw (self, to: &mut Tui) -> Usually> { - if let Some(e) = self.error.read().unwrap().as_ref() { - to.show(to.area(), e); - } - for (index, dsl) in self.mode.view.iter().enumerate() { - if let Err(e) = self.understand(to, dsl) { - *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); - break; - } - } - } - } - - impl<'a> Namespace<'a, AppCommand> for App { - symbols!('a |app| -> AppCommand { - "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, - "x/dec" => AppCommand::Dec { axis: ControlAxis::X }, - "y/inc" => AppCommand::Inc { axis: ControlAxis::Y }, - "y/dec" => AppCommand::Dec { axis: ControlAxis::Y }, - "confirm" => AppCommand::Confirm, - "cancel" => AppCommand::Cancel, - }); - } - - impl Understand for App { - fn understand_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { - app_understand_expr(self, to, lang) - } - fn understand_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { - app_understand_word(self, to, lang) - } - } - - fn app_understand_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> { - if evaluate_output_expression(state, to, lang)? - || evaluate_output_expression_tui(state, to, lang)? { - Ok(()) - } else { - Err(format!("App::understand_expr: unexpected: {lang:?}").into()) - } - } - - fn app_understand_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> { - let mut frags = dsl.src()?.unwrap().split("/"); - match frags.next() { - Some(":logo") => to.place(&view_logo()), - Some(":status") => to.place(&h_exact(1, "TODO: Status Bar")), - Some(":meters") => match frags.next() { - Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters")))), - Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters")))), - _ => panic!() - }, - Some(":tracks") => match frags.next() { - None => to.place(&"TODO tracks"), - Some("names") => to.place(&state.project.view_track_names(state.color.clone())),//Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), - Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs")))), - Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices")))), - Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs")))), - _ => panic!() - }, - Some(":scenes") => match frags.next() { - None => to.place(&"TODO scenes"), - Some(":scenes/names") => to.place(&"TODO Scene Names"), - _ => panic!() - }, - Some(":editor") => to.place(&"TODO Editor"), - Some(":dialog") => match frags.next() { - Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &state.dialog { - let items = items.clone(); - let selected = selected; - Some(wh_full(Thunk::new(move|to: &mut Tui|{ - for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - to.place(&y_push((2 * index) as u16, - Tui::fg_bg( - if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, - if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, - h_exact(2, origin_n(w_full(item))) - ))); - } - }))) - } else { - None - }), - _ => unimplemented!("App::understand_word: {dsl:?} ({frags:?})"), - }, - Some(":templates") => to.place(&{ - let modes = state.config.modes.clone(); - let height = (modes.read().unwrap().len() * 2) as u16; - h_exact(height, w_min(30, Thunk::new(move |to: &mut Tui|{ - for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { - let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; - let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); - let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); - let fg1 = Rgb(224, 192, 128); - let fg2 = Rgb(224, 128, 32); - let field_name = w_full(origin_w(Tui::fg(fg1, name))); - let field_id = w_full(origin_e(Tui::fg(fg2, id))); - let field_info = w_full(origin_w(info)); - to.place(&y_push((2 * index) as u16, - h_exact(2, w_full(Tui::bg(bg, south( - above(field_name, field_id), field_info)))))); - } - }))) - }), - Some(":sessions") => to.place(&h_exact(6, w_min(30, Thunk::new(|to: &mut Tui|{ - let fg = Rgb(224, 192, 128); - for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; - to.place(&y_push((2 * index) as u16, - &h_exact(2, w_full(Tui::bg(bg, origin_w(Tui::fg(fg, name))))))); - } - })))), - Some(":browse/title") => to.place(&w_full(origin_w(field_v(ItemColor::default(), - match state.dialog.browser_target().unwrap() { - BrowseTarget::SaveProject => "Save project:", - BrowseTarget::LoadProject => "Load project:", - BrowseTarget::ImportSample(_) => "Import sample:", - BrowseTarget::ExportSample(_) => "Export sample:", - BrowseTarget::ImportClip(_) => "Import clip:", - BrowseTarget::ExportClip(_) => "Export clip:", - }, w_shrink(3, h_exact(1, Tui::fg(Tui::g(96), x_repeat("🭻")))))))), - Some(":device") => { - let selected = state.dialog.device_kind().unwrap(); - to.place(&south(Tui::bold(true, "Add device"), iter_south(1, - move||device_kinds().iter(), - move|_label: &&'static str, i|{ - let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let lb = if i == selected { "[ " } else { " " }; - let rb = if i == selected { " ]" } else { " " }; - w_full(Tui::bg(bg, east(lb, west(rb, "FIXME device name")))) }))) - }, - Some(":debug") => to.place(&h_exact(1, format!("[{:?}]", to.area()))), - Some(_) => { - let views = state.config.views.read().unwrap(); - if let Some(dsl) = views.get(dsl.src()?.unwrap()) { - let dsl = dsl.clone(); - std::mem::drop(views); - state.understand(to, &dsl)? - } else { - unimplemented!("{dsl:?}"); - } - }, - _ => unreachable!() - } - Ok(()) - } - - impl App { - /// Update memoized render of clock values. - /// ``` - /// tek::App::default().update_clock(); - /// ``` - pub fn update_clock (&self) { - ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) - } - - /// Set modal dialog. - /// - /// ``` - /// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome()); - /// ``` - pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { - std::mem::swap(&mut self.dialog, &mut dialog); - dialog - } - - /// FIXME: generalize. Set picked device in device pick dialog. - /// - /// ``` - /// tek::App::default().device_pick(0); - /// ``` - pub fn device_pick (&mut self, index: usize) { - self.dialog = Dialog::Device(index); - } - - /// FIXME: generalize. Add device to current track. - pub fn add_device (&mut self, index: usize) -> Usually<()> { - match index { - 0 => { - let name = self.jack.with_client(|c|c.name().to_string()); - let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); - let track = self.track().expect("no active track"); - let port = format!("{}/Sampler", &track.name); - let connect = Connect::exact(format!("{name}:{midi}")); - let sampler = if let Ok(sampler) = Sampler::new( - &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] - ) { - self.dialog = Dialog::None; - Device::Sampler(sampler) - } else { - self.dialog = Dialog::Message("Failed to add device.".into()); - return Err("failed to add device".into()) - }; - let track = self.track_mut().expect("no active track"); - track.devices.push(sampler); - Ok(()) - }, - 1 => { - todo!(); - //Ok(()) - }, - _ => unreachable!(), - } - } - - /// Return reference to content browser if open. - /// - /// ``` - /// assert_eq!(tek::App::default().browser(), None); - /// ``` - pub fn browser (&self) -> Option<&Browse> { - if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } - } - - /// Is a MIDI editor currently focused? - /// - /// ``` - /// tek::App::default().editor_focused(); - /// ``` - pub fn editor_focused (&self) -> bool { - false - } - - /// Toggle MIDI editor. - /// - /// ``` - /// tek::App::default().toggle_editor(None); - /// ``` - pub fn toggle_editor (&mut self, value: Option) { - //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); - let value = value.unwrap_or_else(||!self.editor().is_some()); - if value { - // Create new clip in pool when entering empty cell - if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && slot.is_none() - && let Some(track) = self.project.tracks.get_mut(track) - { - let (_index, clip) = self.pool.add_new_clip(); - // autocolor: new clip colors from scene and track color - let color = track.color.base.mix(scene.color.base, 0.5); - clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); - if let Some(editor) = &mut self.project.editor { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip.clone()); - //Some(clip) - } else { - //None - } - } else if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && let Some(clip) = slot.as_mut() - { - // Remove clip from arrangement when exiting empty clip editor - let mut swapped = None; - if clip.read().unwrap().count_midi_messages() == 0 { - std::mem::swap(&mut swapped, slot); - } - if let Some(clip) = swapped { - self.pool.delete_clip(&clip.read().unwrap()); - } - } - } - } -} - -#[cfg(feature = "vst2")] impl ::vst::host::Host for Plugin {} - -mod arrange { - use crate::*; - impl_has!(Jack<'static>: |self: Arrangement| self.jack); - impl_has!(Measure: |self: Arrangement| self.size); - impl_has!(Vec: |self: Arrangement| self.tracks); - impl_has!(Vec: |self: Arrangement| self.scenes); - impl_has!(Vec: |self: Arrangement| self.midi_ins); - impl_has!(Vec: |self: Arrangement| self.midi_outs); - impl_has!(Clock: |self: Arrangement| self.clock); - impl_has!(Selection: |self: Arrangement| self.selection); - impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); - impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); - impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); - impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); - impl Arrangement { - /// Create a new arrangement. - pub fn new ( - jack: &Jack<'static>, - name: Option>, - clock: Clock, - tracks: Vec, - scenes: Vec, - midi_ins: Vec, - midi_outs: Vec, - ) -> Self { - Self { - clock, tracks, scenes, midi_ins, midi_outs, - jack: jack.clone(), - name: name.unwrap_or_default(), - color: ItemTheme::random(), - selection: Selection::TrackClip { track: 0, scene: 0 }, - ..Default::default() - } - } - /// Width of display - pub fn w (&self) -> u16 { - self.size.w() as u16 - } - /// Width allocated for sidebar. - pub fn w_sidebar (&self, is_editing: bool) -> u16 { - self.w() / if is_editing { 16 } else { 8 } as u16 - } - /// Width available to display tracks. - pub fn w_tracks_area (&self, is_editing: bool) -> u16 { - self.w().saturating_sub(self.w_sidebar(is_editing)) - } - /// Height of display - pub fn h (&self) -> u16 { - self.size.h() as u16 - } - /// Height taken by visible device slots. - pub fn h_devices (&self) -> u16 { - 2 - //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) - } - /// Add multiple tracks - #[cfg(feature = "track")] pub fn tracks_add ( - &mut self, - count: usize, width: Option, - mins: &[Connect], mouts: &[Connect], - ) -> Usually<()> { - let track_color_1 = ItemColor::random(); - let track_color_2 = ItemColor::random(); - for i in 0..count { - let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); - let track = self.track_add(None, Some(color), mins, mouts)?.1; - if let Some(width) = width { - track.width = width; - } - } - Ok(()) - } - /// Add a track - #[cfg(feature = "track")] pub fn track_add ( - &mut self, - name: Option<&str>, color: Option, - mins: &[Connect], mouts: &[Connect], - ) -> Usually<(usize, &mut Track)> { - let name: Arc = name.map_or_else( - ||format!("trk{:02}", self.track_last).into(), - |x|x.to_string().into() - ); - self.track_last += 1; - let track = Track { - width: (name.len() + 2).max(12), - color: color.unwrap_or_else(ItemTheme::random), - sequencer: Sequencer::new( - &format!("{name}"), - self.jack(), - Some(self.clock()), - None, - mins, - mouts - )?, - name, - ..Default::default() - }; - self.tracks_mut().push(track); - let len = self.tracks().len(); - let index = len - 1; - for scene in self.scenes_mut().iter_mut() { - while scene.clips.len() < len { - scene.clips.push(None); - } - } - Ok((index, &mut self.tracks_mut()[index])) - } - #[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { - south( - h_exact(1, self.view_inputs_header()), - Thunk::new(|to: &mut Tui|{ - for (index, port) in self.midi_ins().iter().enumerate() { - to.place(&x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port)))) - } - }) - ) - } - #[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Draw + '_ { - east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), - west(w_exact(4, button_2("I", "+", false)), Thunk::new(move|to: &mut Tui|for (_index, track, x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - to.place(&x_push(x1 as u16, Tui::bg(track.color.dark.rgb, origin_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), - either(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), - either(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), - )))))) - }))) - } - #[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Draw { - east(w_exact(20, origin_w(east(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), - west(w_exact(4, ()), Thunk::new(move|to: &mut Tui|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - to.place(&Tui::bg(track.color.darker.rgb, origin_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), - either(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), - either(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), - ))))) - }))) - } - #[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { - let mut h = 1; - for output in self.midi_outs().iter() { - h += 1 + output.connections.len(); - } - let h = h as u16; - let list = south( - h_exact(1, w_full(origin_w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), - h_exact(h - 1, wh_full(origin_nw(Thunk::new(|to: &mut Tui|{ - for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&h_exact(1,w_full(east( - origin_w(east(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), - w_full(origin_e(format!("{}/{} ", - port.port().get_connections().len(), - port.connections.len()))))))); - for (index, conn) in port.connections.iter().enumerate() { - to.place(&h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info()))))); - } - } - }))))); - h_exact(h, view_track_row_section(theme, list, button_2("O", "+", false), - Tui::bg(theme.darker.rgb, origin_w(w_full( - Thunk::new(|to: &mut Tui|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&w_exact(track_width(index, track), - Thunk::new(|to: &mut Tui|{ - to.place(&h_exact(1, origin_w(east( - either(true, Tui::fg(Green, "play "), "play "), - either(false, Tui::fg(Yellow, "solo "), "solo "), - )))); - for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&h_exact(1, origin_w(east( - either(true, Tui::fg(Green, " ● "), " · "), - either(false, Tui::fg(Yellow, " ● "), " · "), - )))); - for (_index, _conn) in port.connections.iter().enumerate() { - to.place(&h_exact(1, w_full(""))); - } - }})))}})))))) - } - #[cfg(feature = "track")] pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { - let mut h = 2u16; - for track in self.tracks().iter() { - h = h.max(track.devices.len() as u16 * 2); - } - view_track_row_section(theme, - button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), - button_2("D", "+", false), - Thunk::new(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&wh_exact(track_width(index, track), h + 1, - Tui::bg(track.color.dark.rgb, origin_nw(iter_south(2, move||0..h, - |_, _index|wh_exact(track.width as u16, 2, - Tui::fg_bg( - ItemTheme::G[32].lightest.rgb, - ItemTheme::G[32].dark.rgb, - origin_nw(format!(" · {}", "--"))))))))); - })) - } - /// Put a clip in a slot - #[cfg(feature = "clip")] pub fn clip_put ( - &mut self, track: usize, scene: usize, clip: Option>> - ) -> Option>> { - let old = self.scenes[scene].clips[track].clone(); - self.scenes[scene].clips[track] = clip; - old - } - /// Change the color of a clip, returning the previous one - #[cfg(feature = "clip")] pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) - -> Option - { - self.scenes[scene].clips[track].as_ref().map(|clip|{ - let mut clip = clip.write().unwrap(); - let old = clip.color.clone(); - clip.color = color.clone(); - panic!("{color:?} {old:?}"); - //old - }) - } - /// Toggle looping for the active clip - #[cfg(feature = "clip")] pub fn toggle_loop (&mut self) { - if let Some(clip) = self.selected_clip() { - clip.write().unwrap().toggle_loop() - } - } - /// Get the first sampler of the active track - #[cfg(feature = "sampler")] pub fn sampler (&self) -> Option<&Sampler> { - self.selected_track()?.sampler(0) - } - /// Get the first sampler of the active track - #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.selected_track_mut()?.sampler_mut(0) - } - } - impl ScenesView for Arrangement { - fn h_scenes (&self) -> u16 { - (self.measure_height() as u16).saturating_sub(20) - } - fn w_side (&self) -> u16 { - (self.measure_width() as u16 * 2 / 10).max(20) - } - fn w_mid (&self) -> u16 { - (self.measure_width() as u16).saturating_sub(2 * self.w_side()).max(40) - } - } - impl HasClipsSize for Arrangement { - fn clips_size (&self) -> &Measure { &self.size_inner } - } -} - -mod browse { - use crate::*; - impl Browse { - pub fn new (cwd: Option) -> Usually { - let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; - let mut dirs = vec![]; - let mut files = vec![]; - for entry in std::fs::read_dir(&cwd)? { - let entry = entry?; - let name = entry.file_name(); - let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); - let meta = entry.metadata()?; - if meta.is_dir() { - dirs.push((name, format!("📁 {decoded}"))); - } else if meta.is_file() { - files.push((name, format!("📄 {decoded}"))); - } - } - Ok(Self { cwd, dirs, files, ..Default::default() }) - } - pub fn chdir (&self) -> Usually { Self::new(Some(self.path())) } - pub fn len (&self) -> usize { self.dirs.len() + self.files.len() } - pub fn is_dir (&self) -> bool { self.index < self.dirs.len() } - pub fn is_file (&self) -> bool { self.index >= self.dirs.len() } - pub fn path (&self) -> PathBuf { - self.cwd.join(if self.is_dir() { - &self.dirs[self.index].0 - } else if self.is_file() { - &self.files[self.index - self.dirs.len()].0 - } else { - unreachable!() - }) - } - fn _todo_stub_path_buf (&self) -> PathBuf { todo!() } - fn _todo_stub_usize (&self) -> usize { todo!() } - fn _todo_stub_arc_str (&self) -> Arc { todo!() } - } - impl Browse { - fn tui (&self) -> impl Draw { - iter_south(1, ||EntriesIterator { - offset: 0, - index: 0, - length: self.dirs.len() + self.files.len(), - browser: self, - }, |entry, _index|w_full(origin_w(entry))) - } - } - impl<'a> Iterator for EntriesIterator<'a> { - type Item = Modify<&'a str>; - fn next (&mut self) -> Option { - let dirs = self.browser.dirs.len(); - let files = self.browser.files.len(); - let index = self.index; - if self.index < dirs { - self.index += 1; - Some(Tui::bold(true, self.browser.dirs[index].1.as_str())) - } else if self.index < dirs + files { - self.index += 1; - Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str())) - } else { - None - } - } - } - impl PartialEq for BrowseTarget { - fn eq (&self, other: &Self) -> bool { - match self { - Self::ImportSample(_) => false, - Self::ExportSample(_) => false, - Self::ImportClip(_) => false, - Self::ExportClip(_) => false, - #[allow(unused)] t => matches!(other, t) - } - } - } -} - -mod clock { - use crate::*; - impl std::fmt::Debug for Clock { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Clock") - .field("timebase", &self.timebase) - .field("chunk", &self.chunk) - .field("quant", &self.quant) - .field("sync", &self.sync) - .field("global", &self.global) - .field("playhead", &self.playhead) - .field("started", &self.started) - .finish() - } - } - impl Clock { - pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { - let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); - let timebase = Arc::new(Timebase::default()); - let clock = Self { - quant: Arc::new(24.into()), - sync: Arc::new(384.into()), - transport: Arc::new(Some(transport)), - chunk: Arc::new((chunk as usize).into()), - global: Arc::new(Moment::zero(&timebase)), - playhead: Arc::new(Moment::zero(&timebase)), - offset: Arc::new(Moment::zero(&timebase)), - started: RwLock::new(None).into(), - timebase, - midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), - midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), - click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), - ..Default::default() - }; - if let Some(bpm) = bpm { - clock.timebase.bpm.set(bpm); - } - Ok(clock) - } - pub fn timebase (&self) -> &Arc { - &self.timebase - } - /// Current sample rate - pub fn sr (&self) -> &SampleRate { - &self.timebase.sr - } - /// Current tempo - pub fn bpm (&self) -> &Bpm { - &self.timebase.bpm - } - /// Current MIDI resolution - pub fn ppq (&self) -> &Ppq { - &self.timebase.ppq - } - /// Next pulse that matches launch sync (for phrase switchover) - pub fn next_launch_pulse (&self) -> usize { - let sync = self.sync.get() as usize; - let pulse = self.playhead.pulse.get() as usize; - if pulse % sync == 0 { - pulse - } else { - (pulse / sync + 1) * sync - } - } - /// Start playing, optionally seeking to a given location beforehand - pub fn play_from (&self, start: Option) -> Usually<()> { - if let Some(transport) = self.transport.as_ref() { - if let Some(start) = start { - transport.locate(start)?; - } - transport.start()?; - } - Ok(()) - } - /// Pause, optionally seeking to a given location afterwards - pub fn pause_at (&self, pause: Option) -> Usually<()> { - if let Some(transport) = self.transport.as_ref() { - transport.stop()?; - if let Some(pause) = pause { - transport.locate(pause)?; - } - } - Ok(()) - } - /// Is currently paused? - pub fn is_stopped (&self) -> bool { - self.started.read().unwrap().is_none() - } - /// Is currently playing? - pub fn is_rolling (&self) -> bool { - self.started.read().unwrap().is_some() - } - /// Update chunk size - pub fn set_chunk (&self, n_frames: usize) { - self.chunk.store(n_frames, Relaxed); - } - pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { - // Store buffer length - self.set_chunk(scope.n_frames() as usize); - - // Store reported global frame and usec - let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; - self.global.sample.set(current_frames as f64); - self.global.usec.set(current_usecs as f64); - - let mut started = self.started.write().unwrap(); - - // If transport has just started or just stopped, - // update starting point: - if let Some(transport) = self.transport.as_ref() { - match (transport.query_state()?, started.as_ref()) { - (TransportState::Rolling, None) => { - let moment = Moment::zero(&self.timebase); - moment.sample.set(current_frames as f64); - moment.usec.set(current_usecs as f64); - *started = Some(moment); - }, - (TransportState::Stopped, Some(_)) => { - *started = None; - }, - _ => {} - }; - } - - self.playhead.update_from_sample(started.as_ref() - .map(|started|current_frames as f64 - started.sample.get()) - .unwrap_or(0.)); - - Ok(()) - } - - pub fn bbt (&self) -> PositionBBT { - let pulse = self.playhead.pulse.get() as i32; - let ppq = self.timebase.ppq.get() as i32; - let bpm = self.timebase.bpm.get(); - let bar = (pulse / ppq) / 4; - PositionBBT { - bar: 1 + bar, - beat: 1 + (pulse / ppq) % 4, - tick: (pulse % ppq), - bar_start_tick: (bar * 4 * ppq) as f64, - beat_type: 4., - beats_per_bar: 4., - beats_per_minute: bpm, - ticks_per_beat: ppq as f64 - } - } - - pub fn next_launch_instant (&self) -> Moment { - Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) - } - - /// Get index of first sample to populate. - /// - /// Greater than 0 means that the first pulse of the clip - /// falls somewhere in the middle of the chunk. - pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ - (scope.last_frame_time() as usize).saturating_sub( - started.sample.get() as usize + - self.started.read().unwrap().as_ref().unwrap().sample.get() as usize - ) - } - - // Get iterator that emits sample paired with pulse. - // - // * Sample: index into output buffer at which to write MIDI event - // * Pulse: index into clip from which to take the MIDI event - // - // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. - pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> Ticker { - self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) - } - } - impl Clock { - fn _todo_provide_u32 (&self) -> u32 { - todo!() - } - fn _todo_provide_opt_u32 (&self) -> Option { - todo!() - } - fn _todo_provide_f64 (&self) -> f64 { - todo!() - } - } - impl Command for ClockCommand { - fn execute (&self, state: &mut T) -> Perhaps { - self.execute(state.clock_mut()) // awesome - } - } - impl ClockView { - pub const BEAT_EMPTY: &'static str = "-.-.--"; - pub const TIME_EMPTY: &'static str = "-.---s"; - pub const BPM_EMPTY: &'static str = "---.---"; - pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { - let rate = clock.timebase.sr.get(); - let chunk = clock.chunk.load(Relaxed) as f64; - let lat = chunk / rate * 1000.; - let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); - let mut cache = cache.write().unwrap(); - cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); - cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); - cache.sr.update(Some((compact, rate)), |buf,_,_|{ - buf.clear(); - if compact { - write!(buf, "{:.1}kHz", rate / 1000.) - } else { - write!(buf, "{:.0}Hz", rate) - } - }); - if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { - let pulse = clock.timebase.usecs_to_pulse(now); - let time = now/1000000.; - let bpm = clock.timebase.bpm.get(); - cache.beat.update(Some(pulse), |buf, _, _|{ - buf.clear(); - clock.timebase.format_beats_1_to(buf, pulse) - }); - cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); - cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); - } else { - cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY)); - cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY)); - cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY)); - } - } - } - impl_default!(ClockView: { - let mut beat = String::with_capacity(16); - let _ = write!(beat, "{}", Self::BEAT_EMPTY); - let mut time = String::with_capacity(16); - let _ = write!(time, "{}", Self::TIME_EMPTY); - let mut bpm = String::with_capacity(16); - let _ = write!(bpm, "{}", Self::BPM_EMPTY); - Self { - beat: Memo::new(None, beat), - time: Memo::new(None, time), - bpm: Memo::new(None, bpm), - sr: Memo::new(None, String::with_capacity(16)), - buf: Memo::new(None, String::with_capacity(16)), - lat: Memo::new(None, String::with_capacity(16)), - } - }); -} - -impl Selection { - pub fn describe ( - &self, - #[cfg(feature = "track")] tracks: &[Track], - #[cfg(feature = "scene")] scenes: &[Scene], - ) -> Arc { - use Selection::*; - format!("{}", match self { - Mix => "Everything".to_string(), - #[cfg(feature = "scene")] Scene(s) => - scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), - #[cfg(feature = "track")] Track(t) => - tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), - TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { - (Some(_), Some(s)) => match s.clip(*track) { - Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), - None => format!("T{track} S{scene}: Empty") - }, - _ => format!("T{track} S{scene}: Empty"), - }, - _ => todo!() - }).into() - } - #[cfg(feature = "scene")] pub fn scene (&self) -> Option { - use Selection::*; - match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } - } - #[cfg(feature = "scene")] pub fn select_scene (&self, scene_count: usize) -> Self { - use Selection::*; - match self { - Mix | Track(_) => Scene(0), - Scene(s) => Scene((s + 1) % scene_count), - TrackClip { scene, .. } => Track(*scene), - _ => todo!(), - } - } - #[cfg(feature = "scene")] pub fn select_scene_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Scene(0), - Track(t) => TrackClip { track: *t, scene: 0 }, - Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, - TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, - _ => todo!() - } - } - #[cfg(feature = "scene")] pub fn select_scene_prev (&self) -> Self { - use Selection::*; - match self { - Mix | Scene(0) => Mix, - Scene(s) => Scene(s - 1), - Track(t) => Track(*t), - TrackClip { track, scene: 0 } => Track(*track), - TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, - _ => todo!() - } - } - #[cfg(feature = "track")] pub fn track (&self) -> Option { - use Selection::*; - if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { - Some(*track) - } else { - None - } - } - #[cfg(feature = "track")] pub fn select_track (&self, track_count: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(_) => Mix, - Track(t) => Track((t + 1) % track_count), - TrackClip { track, .. } => Track(*track), - _ => todo!(), - } - } - #[cfg(feature = "track")] pub fn select_track_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(s) => TrackClip { track: 0, scene: *s }, - Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, - TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, - _ => todo!() - } - } - #[cfg(feature = "track")] pub fn select_track_prev (&self) -> Self { - use Selection::*; - match self { - Mix => Mix, - Scene(s) => Scene(*s), - Track(0) => Mix, - Track(t) => Track(t - 1), - TrackClip { track: 0, scene } => Scene(*scene), - TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, - _ => todo!() - } - } -} - - - -/// Default is always empty map regardless if `E` and `C` implement [Default]. -impl Default for Bind { - fn default () -> Self { Self(Default::default()) } -} -impl Default for Binding { - fn default () -> Self { - Self { - commands: Default::default(), - condition: Default::default(), - description: Default::default(), - source: Default::default(), - } - } -} - -impl_default!(AppCommand: Self::Nop); -impl_default!(MenuItem: Self("".into(), Arc::new(Box::new(|_|Ok(()))))); -impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ)); - -impl Gettable for AtomicBool { fn get (&self) -> bool { self.load(Relaxed) } } -impl InteriorMutable for AtomicBool { fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) } } -impl Gettable for AtomicUsize { fn get (&self) -> usize { self.load(Relaxed) } } -impl InteriorMutable for AtomicUsize { fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) } } - -impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } -impl AsRef> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } } -impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.size_inner } } -impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } - -#[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock); - -impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); -impl_debug!(Condition |self, w| { write!(w, "*") }); - -primitive!(u8: try_to_u8); -primitive!(u16: try_to_u16); -primitive!(usize: try_to_usize); -primitive!(isize: try_to_isize); -namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); -namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); -namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| { - ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), - ":h/sample-detail" => 6.max(app.measure_height() as u16 * 3 / 9), }; }); -namespace!(App: isize { literal = |dsl|try_to_isize(dsl); }); -namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| { - ":scene-count" => app.scenes().len(), - ":track-count" => app.tracks().len(), - ":device-kind" => app.dialog.device_kind().unwrap_or(0), - ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), - ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), }; }); -namespace!(App: bool { symbol = |app| { // Provide boolean values. - ":mode/editor" => app.project.editor.is_some(), - ":focused/dialog" => !matches!(app.dialog, Dialog::None), - ":focused/message" => matches!(app.dialog, Dialog::Message(..)), - ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), - ":focused/browser" => app.dialog.browser().is_some(), - ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), - ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), - ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), - ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), - ":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}), - ":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)), - ":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)), - ":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix), -}; }); -namespace!(App: ItemTheme {}); // TODO: provide colors here -namespace!(App: Dialog { symbol = |app| { - ":dialog/none" => Dialog::None, - ":dialog/options" => Dialog::Options, - ":dialog/device" => Dialog::Device(0), - ":dialog/device/prev" => Dialog::Device(0), - ":dialog/device/next" => Dialog::Device(0), - ":dialog/help" => Dialog::Help(0), - ":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, - Browse::new(None).unwrap().into()), - ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, - Browse::new(None).unwrap().into()), - ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), - Browse::new(None).unwrap().into()), -}; }); -namespace!(App: Selection { symbol = |app| { - ":select/scene" => app.selection().select_scene(app.tracks().len()), - ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), - ":select/scene/prev" => app.selection().select_scene_prev(), - ":select/track" => app.selection().select_track(app.tracks().len()), - ":select/track/next" => app.selection().select_track_next(app.tracks().len()), - ":select/track/prev" => app.selection().select_track_prev(), -}; }); -namespace!(App: Color { - symbol = |app| { - ":color/bg" => Color::Rgb(28, 32, 36), - }; - expression = |app| { - "g" (n: u8) => Color::Rgb(n, n, n), - "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), - }; -}); -namespace!(App: Option { symbol = |app| { - ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) -}; }); -namespace!(App: Option { symbol = |app| { - ":selected/scene" => app.selection().scene(), - ":selected/track" => app.selection().track(), -}; }); -namespace!(App: Option>> { - symbol = |app| { - ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { - app.scenes()[*scene].clips[*track].clone() - } else { - None - } - }; -}); - -mod time { - use crate::*; - impl Moment { - pub fn zero (timebase: &Arc) -> Self { - Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } - } - pub fn from_usec (timebase: &Arc, usec: f64) -> Self { - Self { - usec: usec.into(), - sample: timebase.sr.usecs_to_sample(usec).into(), - pulse: timebase.usecs_to_pulse(usec).into(), - timebase: timebase.clone(), - } - } - pub fn from_sample (timebase: &Arc, sample: f64) -> Self { - Self { - sample: sample.into(), - usec: timebase.sr.samples_to_usec(sample).into(), - pulse: timebase.samples_to_pulse(sample).into(), - timebase: timebase.clone(), - } - } - pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { - Self { - pulse: pulse.into(), - sample: timebase.pulses_to_sample(pulse).into(), - usec: timebase.pulses_to_usec(pulse).into(), - timebase: timebase.clone(), - } - } - #[inline] pub fn update_from_usec (&self, usec: f64) { - self.usec.set(usec); - self.pulse.set(self.timebase.usecs_to_pulse(usec)); - self.sample.set(self.timebase.sr.usecs_to_sample(usec)); - } - #[inline] pub fn update_from_sample (&self, sample: f64) { - self.usec.set(self.timebase.sr.samples_to_usec(sample)); - self.pulse.set(self.timebase.samples_to_pulse(sample)); - self.sample.set(sample); - } - #[inline] pub fn update_from_pulse (&self, pulse: f64) { - self.usec.set(self.timebase.pulses_to_usec(pulse)); - self.pulse.set(pulse); - self.sample.set(self.timebase.pulses_to_sample(pulse)); - } - #[inline] pub fn format_beat (&self) -> Arc { - self.timebase.format_beats_1(self.pulse.get()).into() - } - } - impl LaunchSync { - pub fn next (&self) -> f64 { - note_duration_next(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - note_duration_prev(self.get() as usize) as f64 - } - } - impl Quantize { - pub fn next (&self) -> f64 { - note_duration_next(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - note_duration_prev(self.get() as usize) as f64 - } - } - impl Timebase { - /// Specify sample rate, BPM and PPQ - pub fn new ( - s: impl Into, - b: impl Into, - p: impl Into - ) -> Self { - Self { sr: s.into(), bpm: b.into(), ppq: p.into() } - } - /// Iterate over ticks between start and end. - #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> Ticker { - Ticker { spp: self.samples_per_pulse(), sample: start, start, end } - } - /// Return the duration fo a beat in microseconds - #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } - /// Return the number of beats in a second - #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } - /// Return the number of microseconds corresponding to a note of the given duration - #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { - 4.0 * self.usec_per_beat() * num / den - } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } - /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) - #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } - /// Return number of pulses in a second (BPM-dependent) - #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } - /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) - #[inline] pub fn pulses_per_sample (&self) -> f64 { - self.usec_per_pulse() / self.sr.usec_per_sample() - } - /// Return number of samples in a pulse (SR- and BPM-dependent) - #[inline] pub fn samples_per_pulse (&self) -> f64 { - self.sr.get() / self.pulses_per_second() - } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { - self.pulses_per_sample() * p - } - /// Convert a number of samples to a pulse number (SR- and BPM-dependent) - #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { - s / self.pulses_per_sample() - } - /// Return the number of samples corresponding to a note of the given duration - #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { - self.usec_to_sample(self.note_to_usec(note)) - } - /// Return the number of samples corresponding to the given number of microseconds - #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { - usec * self.sr.get() / 1000f64 - } - /// Return the quantized position of a moment in time given a step - #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { - let step = self.note_to_usec(step); - (time / step, time % step) - } - /// Quantize a collection of events - #[inline] pub fn quantize_into + Sized, T> ( - &self, step: (f64, f64), events: E - ) -> Vec<(f64, f64)> { - events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 0 - #[inline] pub fn format_beats_0 (&self, pulse: f64) -> Arc { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - format!("{}.{}.{pulses:02}", beats / 4, beats % 4).into() - } - /// Format a number of pulses into Beat.Bar starting from 0 - #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4, beats % 4).into() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc { - let mut string = String::with_capacity(16); - self.format_beats_1_to(&mut string, pulse).expect("failed to format {pulse} into beat"); - string.into() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1_to (&self, w: &mut impl std::fmt::Write, pulse: f64) -> Result<(), std::fmt::Error> { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - write!(w, "{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4 + 1, beats % 4 + 1).into() - } - } - impl SampleRate { - /// Return the duration of a sample in microseconds (floating) - #[inline] pub fn usec_per_sample (&self) -> f64 { - 1_000_000f64 / self.get() - } - /// Return the duration of a sample in microseconds (floating) - #[inline] pub fn sample_per_usec (&self) -> f64 { - self.get() / 1_000_000f64 - } - /// Convert a number of samples to microseconds (floating) - #[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 { - self.usec_per_sample() * samples - } - /// Convert a number of microseconds to samples (floating) - #[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 { - self.sample_per_usec() * usecs - } - } - impl Microsecond { - #[inline] pub fn format_msu (&self) -> Arc { - let usecs = self.get() as usize; - let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); - let (minutes, seconds) = (seconds / 60, seconds % 60); - format!("{minutes}:{seconds:02}:{msecs:03}").into() - } - } - - impl_time_unit!(SampleCount); - impl_time_unit!(SampleRate); - impl_time_unit!(Microsecond); - impl_time_unit!(Quantize); - impl_time_unit!(Ppq); - impl_time_unit!(Pulse); - impl_time_unit!(Bpm); - impl_time_unit!(LaunchSync); -} - -mod midi { - use crate::*; - impl_from!(MidiSelection: |data:(usize, bool)| Self { - time_len: Arc::new(0.into()), - note_axis: Arc::new(0.into()), - note_lo: Arc::new(0.into()), - time_axis: Arc::new(0.into()), - time_start: Arc::new(0.into()), - time_zoom: Arc::new(data.0.into()), - time_lock: Arc::new(data.1.into()), - }); - impl_default!(MidiCursor: Self { - time_pos: Arc::new(0.into()), - note_pos: Arc::new(36.into()), - note_len: Arc::new(24.into()), - }); - - impl NotePoint for MidiCursor { - fn note_len (&self) -> &AtomicUsize { - &self.note_len - } - fn note_pos (&self) -> &AtomicUsize { - &self.note_pos - } - } - - impl TimePoint for MidiCursor { - fn time_pos (&self) -> &AtomicUsize { - self.time_pos.as_ref() - } - } - - impl TimeRange for MidiSelection { - fn time_len (&self) -> &AtomicUsize { &self.time_len } - fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } - fn time_lock (&self) -> &AtomicBool { &self.time_lock } - fn time_start (&self) -> &AtomicUsize { &self.time_start } - fn time_axis (&self) -> &AtomicUsize { &self.time_axis } - } - - impl NoteRange for MidiSelection { - fn note_lo (&self) -> &AtomicUsize { &self.note_lo } - fn note_axis (&self) -> &AtomicUsize { &self.note_axis } - } - - impl Iterator for Ticker { - type Item = (usize, usize); - fn next (&mut self) -> Option { - loop { - if self.sample > self.end { return None } - let spp = self.spp; - let sample = self.sample as f64; - let start = self.start; - let end = self.end; - self.sample += 1; - //println!("{spp} {sample} {start} {end}"); - let jitter = sample.rem_euclid(spp); // ramps - let next_jitter = (sample + 1.0).rem_euclid(spp); - if jitter > next_jitter { // at crossing: - let time = (sample as usize) % (end as usize-start as usize); - let tick = (sample / spp) as usize; - return Some((time, tick)) - } - } - } - } - - impl JackPort for MidiInput { - type Port = MidiIn; - type Pair = MidiOut; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec(), - held: Arc::new(RwLock::new([false;128])) - }; - port.connect_to_matching()?; - Ok(port) - } - } - - impl JackPort for MidiOutput { - type Port = MidiOut; - type Pair = MidiIn; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self::register(jack, name)?; - let jack = jack.clone(); - let name = name.as_ref().into(); - let connections = connect.to_vec(); - let port = Self { - jack, - port, - name, - connections, - held: Arc::new([false;128].into()), - note_buffer: vec![0;8], - output_buffer: vec![vec![];65536], - }; - port.connect_to_matching()?; - Ok(port) - } - } - - impl MidiOutput { - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { - let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); - for frame in &mut self.output_buffer[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(&mut self.output_buffer); - } - } - /// Write a note to the output buffer - pub fn buffer_write <'a> ( - &'a mut self, - sample: usize, - event: LiveEvent, - ) { - self.note_buffer.fill(0); - event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); - self.output_buffer[sample].push(self.note_buffer.clone()); - // Update the list of currently held notes. - if let LiveEvent::Midi { ref message, .. } = event { - update_keys(&mut*self.held.write().unwrap(), message); - } - } - /// Write a chunk of MIDI data from the output buffer to the output port. - pub fn buffer_emit (&mut self, scope: &ProcessScope) { - let samples = scope.n_frames() as usize; - let mut writer = self.port.writer(scope); - for (time, events) in self.output_buffer.iter().enumerate().take(samples) { - for bytes in events.iter() { - writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ - panic!("Failed to write MIDI data: {bytes:?}"); - }); - } - } - } - } - impl MidiInput { - pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { - parse_midi_input(self.port().iter(scope)) - } - } - impl> + AsMut>> HasMidiIns for T { - fn midi_ins (&self) -> &Vec { self.as_ref() } - fn midi_ins_mut (&mut self) -> &mut Vec { self.as_mut() } - } - impl> + AsMut>> HasMidiOuts for T { - fn midi_outs (&self) -> &Vec { self.as_ref() } - fn midi_outs_mut (&mut self) -> &mut Vec { self.as_mut() } - } - impl> AddMidiIn for T { - fn midi_in_add (&mut self) -> Usually<()> { - let index = self.midi_ins().len(); - let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; - self.midi_ins_mut().push(port); - Ok(()) - } - } - /// Trail for thing that may gain new MIDI ports. - impl> AddMidiOut for T { - fn midi_out_add (&mut self) -> Usually<()> { - let index = self.midi_outs().len(); - let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; - self.midi_outs_mut().push(port); - Ok(()) - } - } - - impl MidiClip { - pub fn new ( - name: impl AsRef, - looped: bool, - length: usize, - notes: Option, - color: Option, - ) -> Self { - Self { - uuid: uuid::Uuid::new_v4(), - name: name.as_ref().into(), - ppq: PPQ, - length, - notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), - looped, - loop_start: 0, - loop_length: length, - percussive: true, - color: color.unwrap_or_else(ItemTheme::random) - } - } - pub fn count_midi_messages (&self) -> usize { - let mut count = 0; - for tick in self.notes.iter() { - count += tick.len(); - } - count - } - pub fn set_length (&mut self, length: usize) { - self.length = length; - self.notes = vec![Vec::with_capacity(16);length]; - } - pub fn duplicate (&self) -> Self { - let mut clone = self.clone(); - clone.uuid = uuid::Uuid::new_v4(); - clone - } - pub fn toggle_loop (&mut self) { self.looped = !self.looped; } - pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { - if pulse >= self.length { panic!("extend clip first") } - self.notes[pulse].push(message); - } - /// Check if a range `start..end` contains MIDI Note On `k` - pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { - for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { - for event in events.iter() { - if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } - } - } - false - } - pub fn stop_all () -> Self { - Self::new( - "Stop", - false, - 1, - Some(vec![vec![MidiMessage::Controller { - controller: 123.into(), - value: 0.into() - }]]), - Some(ItemColor::from_tui(Color::Rgb(32, 32, 32)).into()) - ) - } - } - - impl PartialEq for MidiClip { - fn eq (&self, other: &Self) -> bool { - self.uuid == other.uuid - } - } - - impl Eq for MidiClip {} - - impl MidiClip { - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_bool_stub_ (&self) -> bool { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } - fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } - } -} - -mod audio { - use crate::*; - - impl JackPort for AudioInput { - type Port = AudioIn; - type Pair = AudioOut; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec() - }; - port.connect_to_matching()?; - Ok(port) - } - } - - impl JackPort for AudioOutput { - type Port = AudioOut; - type Pair = AudioIn; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec() - }; - port.connect_to_matching()?; - Ok(port) - } - } - - mod meter { - use crate::*; - - impl Draw for RmsMeter { - fn draw(self, to: &mut Tui) -> Usually> { - let XYWH(x, y, w, h) = to.area(); - let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); - } - } - } - } - - impl Draw for Log10Meter { - fn draw(self, to: &mut Tui) -> Usually> { - let XYWH(x, y, w, h) = to.area(); - let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32 / 100.0).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2 - y, Some(Style::default().green())); - } - } - } - } - - fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { - Tui::bg(Black, w_exact(2, iter_east(1, ||meters.iter(), |value, _index|{ - h_full(RmsMeter(*value)) - }))) - } - } -} - -#[cfg(feature = "track")] mod track { - use crate::*; - impl_as_ref!(Vec: |self: App| self.project.as_ref()); - impl_as_mut!(Vec: |self: App| self.project.as_mut()); - #[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt()); - #[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt()); - impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } - impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } } - - impl HasWidth for Track { - const MIN_WIDTH: usize = 9; - fn width_inc (&mut self) { self.width += 1; } - fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } - } - - impl Track { - /// Create a new track with only the default [Sequencer]. - pub fn new ( - name: &impl AsRef, - color: Option, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - ) -> Usually { - Ok(Self { - name: name.as_ref().into(), - color: color.unwrap_or_default(), - sequencer: Sequencer::new(format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to)?, - ..Default::default() - }) - } - pub fn audio_ins (&self) -> &[AudioInput] { - self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() - } - pub fn audio_outs (&self) -> &[AudioOutput] { - self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() - } - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } - pub fn per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + 'a { - iter(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - iter_east(x1 as u16, width, w_exact(width, Tui::fg_bg( - track.color.lightest.rgb, - track.color.base.rgb, - callback(index, track))))}) - } - /// Create a new track connecting the [Sequencer] to a [Sampler]. - #[cfg(feature = "sampler")] pub fn new_with_sampler ( - name: &impl AsRef, - color: Option, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - audio_from: &[&[Connect];2], - audio_to: &[&[Connect];2], - ) -> Usually { - let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; - let client_name = jack.with_client(|c|c.name().to_string()); - let port_name = track.sequencer.midi_outs[0].port_name(); - let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; - track.devices.push(Device::Sampler(Sampler::new( - jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to - )?)); - Ok(track) - } - #[cfg(feature = "sampler")] pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { - for device in self.devices.iter() { - match device { - Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, - _ => {} - } - } - None - } - #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { - for device in self.devices.iter_mut() { - match device { - Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, - _ => {} - } - } - None - } - } - - pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + 'a { - per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track)))) - } - - pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + 'a { - origin_x(Tui::bg(Reset, iter(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - iter_east(x1 as u16, width, w_exact(width, Tui::fg_bg( - track.color.lightest.rgb, - track.color.base.rgb, - callback(index, track))))}))) - } -} - -#[cfg(feature = "scene")] mod scene { - use crate::*; - #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt()); - #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); - #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene()); - #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut()); - impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } - impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } - impl ScenesView for App { - fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(self.w_side()) } - fn w_side (&self) -> u16 { 20 } - fn h_scenes (&self) -> u16 { (self.measure_height() as u16).saturating_sub(20) } - } - impl Scene { - /// Returns the pulse length of the longest clip in the scene - pub fn pulses (&self) -> usize { - self.clips.iter().fold(0, |a, p|{ - a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) - }) - } - /// Returns true if all clips in the scene are - /// currently playing on the given collection of tracks. - pub fn is_playing (&self, tracks: &[Track]) -> bool { - self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() - .all(|(track_index, clip)|match clip { - Some(c) => tracks - .get(track_index) - .map(|track|{ - if let Some((_, Some(clip))) = track.sequencer().play_clip() { - *clip.read().unwrap() == *c.read().unwrap() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) - } - pub fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } - } - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } - } -} - -#[cfg(feature = "sequencer")] mod sequencer { - use crate::*; - impl_has!(Sequencer: |self: Track| self.sequencer); - impl_has!(Clock: |self: Sequencer| self.clock); - impl_has!(Vec: |self: Sequencer| self.midi_ins); - impl_has!(Vec: |self: Sequencer| self.midi_outs); - impl_has!(Measure: |self: MidiEditor| self.size); - impl_has!(Measure: |self: PianoHorizontal| self.size); - impl_default!(Sequencer: Self { - clock: Clock::default(), - play_clip: None, - next_clip: None, - midi_ins: vec![], - midi_outs: vec![], - recording: false, - monitoring: true, - overdub: false, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - note_buf: vec![0;8], - midi_buf: vec![], - reset: true, - }); - impl Sequencer { - pub fn new ( - name: impl AsRef, - jack: &Jack<'static>, - #[cfg(feature = "clock")] clock: Option<&Clock>, - #[cfg(feature = "clip")] clip: Option<&Arc>>, - #[cfg(feature = "port")] midi_from: &[Connect], - #[cfg(feature = "port")] midi_to: &[Connect], - ) -> Usually { - let _name = name.as_ref(); - #[cfg(feature = "clock")] let clock = clock.cloned().unwrap_or_default(); - Ok(Self { - reset: true, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - #[cfg(feature = "port")] midi_ins: vec![MidiInput::new(jack, &format!("M/{}", name.as_ref()), midi_from)?,], - #[cfg(feature = "port")] midi_outs: vec![MidiOutput::new(jack, &format!("{}/M", name.as_ref()), midi_to)?, ], - #[cfg(feature = "clip")] play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), - #[cfg(feature = "clock")] clock, - ..Default::default() - }) - } - fn process_rolling (&mut self, scope: &ProcessScope) -> Control { - self.process_clear(scope, false); - // Write chunk of clip to output, handle switchover - if self.process_playback(scope) { - self.process_switchover(scope); - } - // Monitor input to output - self.process_monitoring(scope); - // Record and/or monitor input - self.process_recording(scope); - // Emit contents of MIDI buffers to JACK MIDI output ports. - self.midi_outs_emit(scope); - Control::Continue - } - fn process_stopped (&mut self, scope: &ProcessScope) -> Control { - if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { - self.process_monitoring(scope) - } - Control::Continue - } - fn process_monitoring (&mut self, scope: &ProcessScope) { - let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat - let monitoring = self.monitoring(); - for input in self.midi_ins.iter() { - for (sample, event, bytes) in input.parsed(scope) { - if let LiveEvent::Midi { message, .. } = event { - if monitoring { - self.midi_buf[sample].push(bytes.to_vec()); - } - // FIXME: don't lock on every event! - update_keys(&mut notes_in.write().unwrap(), &message); - } - } - } - } - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { - let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); - for frame in &mut self.midi_buf_mut()[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(self.midi_buf_mut()); - } - for port in self.midi_outs_mut().iter_mut() { - // Clear output buffer(s) - port.buffer_clear(scope, false); - } - } - fn process_recording (&mut self, scope: &ProcessScope) { - if self.monitoring() { - self.monitor(scope); - } - if let Some((started, ref clip)) = self.play_clip.clone() { - self.record_clip(scope, started, clip); - } - if let Some((_start_at, _clip)) = &self.next_clip() { - self.record_next(); - } - } - fn process_playback (&mut self, scope: &ProcessScope) -> bool { - // If a clip is playing, write a chunk of MIDI events from it to the output buffer. - // If no clip is playing, prepare for switchover immediately. - if let Some((started, clip)) = &self.play_clip { - // Length of clip, to repeat or stop on end. - let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); - // Index of first sample to populate. - let offset = self.clock().get_sample_offset(scope, &started); - // Write MIDI events from clip at sample offsets corresponding to pulses. - for (sample, pulse) in self.clock().get_pulses(scope, offset) { - // If a next clip is enqueued, and we're past the end of the current one, - // break the loop here (FIXME count pulse correctly) - let past_end = if clip.is_some() { pulse >= length } else { true }; - // Is it time for switchover? - if self.next_clip().is_some() && past_end { - return true - } - // If there's a currently playing clip, output notes from it to buffer: - if let Some(clip) = clip { - // Source clip from which the MIDI events will be taken. - let clip = clip.read().unwrap(); - // Clip with zero length is not processed - if clip.length > 0 { - // Current pulse index in source clip - let pulse = pulse % clip.length; - // Output each MIDI event from clip at appropriate frames of output buffer: - for message in clip.notes[pulse].iter() { - for port in self.midi_outs.iter_mut() { - port.buffer_write(sample, LiveEvent::Midi { - channel: 0.into(), /* TODO */ - message: *message - }); - } - } - } - } - } - false - } else { - true - } - } - /// Handle switchover from current to next playing clip. - fn process_switchover (&mut self, scope: &ProcessScope) { - let _midi_buf = self.midi_buf_mut(); - let sample0 = scope.last_frame_time() as usize; - //let samples = scope.n_frames() as usize; - if let Some((start_at, clip)) = &self.next_clip() { - let start = start_at.sample.get() as usize; - let sample = self.clock().started.read().unwrap() - .as_ref().unwrap().sample.get() as usize; - // If it's time to switch to the next clip: - if start <= sample0.saturating_sub(sample) { - // Samples elapsed since clip was supposed to start - let _skipped = sample0 - start; - // Switch over to enqueued clip - let started = Moment::from_sample(self.clock().timebase(), start as f64); - // Launch enqueued clip - *self.play_clip_mut() = Some((started, clip.clone())); - // Unset enqueuement (TODO: where to implement looping?) - *self.next_clip_mut() = None; - // Fill in remaining ticks of chunk from next clip. - self.process_playback(scope); - } - } - } - } - impl HasMidiBuffers for Sequencer { - fn note_buf_mut (&mut self) -> &mut Vec { &mut self.note_buf } - fn midi_buf_mut (&mut self) -> &mut Vec>> { &mut self.midi_buf } - } - impl std::fmt::Debug for Sequencer { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Sequencer") - .field("clock", &self.clock) - .field("play_clip", &self.play_clip) - .field("next_clip", &self.next_clip) - .finish() - } - } - impl MidiMonitor for Sequencer { - fn monitoring (&self) -> bool { self.monitoring } - fn monitoring_mut (&mut self) -> &mut bool { &mut self.monitoring } - fn notes_in (&self) -> &Arc> { &self.notes_in } - } - impl MidiRecord for Sequencer { - fn recording (&self) -> bool { self.recording } - fn recording_mut (&mut self) -> &mut bool { &mut self.recording } - fn overdub (&self) -> bool { self.overdub } - fn overdub_mut (&mut self) -> &mut bool { &mut self.overdub } - } - #[cfg(feature="clip")] impl HasPlayClip for Sequencer { - fn reset (&self) -> bool { self.reset } - fn reset_mut (&mut self) -> &mut bool { &mut self.reset } - fn play_clip (&self) -> &Option<(Moment, Option>>)> { - &self.play_clip - } - fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.play_clip - } - fn next_clip (&self) -> &Option<(Moment, Option>>)> { - &self.next_clip - } - fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.next_clip - } - } - /// JACK process callback for a sequencer's clip sequencer/recorder. - impl Audio for Sequencer { - fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - if self.clock().is_rolling() { - self.process_rolling(scope) - } else { - self.process_stopped(scope) - } - } - } - impl Draw for MidiEditor { - fn draw(self, to: &mut Tui) -> Usually> { - self.tui().draw(to) - } - } - impl Draw for PianoHorizontal { - fn draw(self, to: &mut Tui) -> Usually> { - self.tui().draw(to) - } - } -} - -#[cfg(feature = "editor")] mod editor { - use crate::*; - impl std::fmt::Debug for MidiEditor { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("MidiEditor").field("mode", &self.mode).finish() - } - } - impl_from!(MidiEditor: |clip: &Arc>| { - let model = Self::from(Some(clip.clone())); - model.redraw(); - model - }); - impl_from!(MidiEditor: |clip: Option>>| { - let mut model = Self::default(); - *model.clip_mut() = clip; - model.redraw(); - model - }); - impl_default!(MidiEditor: Self { - size: Measure::new(0, 0), mode: PianoHorizontal::new(None) - }); - impl_default!(OctaveVertical: Self { - on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] - }); - impl MidiEditor { - /// Put note at current position - pub fn put_note (&mut self, advance: bool) { - let mut redraw = false; - if let Some(clip) = self.clip() { - let mut clip = clip.write().unwrap(); - let note_start = self.get_time_pos(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let note_end = note_start + (note_len.saturating_sub(1)); - let key: u7 = u7::from(note_pos as u8); - let vel: u7 = 100.into(); - let length = clip.length; - let note_end = note_end % length; - let note_on = MidiMessage::NoteOn { key, vel }; - if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { - clip.notes[note_start].push(note_on); - } - let note_off = MidiMessage::NoteOff { key, vel }; - if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { - clip.notes[note_end].push(note_off); - } - if advance { - self.set_time_pos((note_end + 1) % clip.length); - } - redraw = true; - } - if redraw { - self.mode.redraw(); - } - } - fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } - fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } - fn note_length (&self) -> usize { self.get_note_len() } - fn note_pos (&self) -> usize { self.get_note_pos() } - fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } - fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } - fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } - fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } - fn note_len (&self) -> usize { self.get_note_len() } - fn note_len_next (&self) -> usize { self.get_note_len() + 1 } - fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } - fn note_range (&self) -> usize { self.get_note_axis() } - fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } - fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } - fn time_zoom (&self) -> usize { self.get_time_zoom() } - fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_lock (&self) -> bool { self.get_time_lock() } - fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } - fn time_pos (&self) -> usize { self.get_time_pos() } - fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } - fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } - fn time_pos_prev (&self) -> usize { - let step = self.get_note_len(); - self.get_time_pos().overflowing_sub(step) - .0.min(self.clip_length().saturating_sub(step)) - } - fn time_pos_prev_fine (&self) -> usize { - self.get_time_pos().overflowing_sub(1) - .0.min(self.clip_length().saturating_sub(1)) - } - pub fn clip_status (&self) -> impl Draw + '_ { - let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.name.clone(), clip.length, clip.looped) - } else { (ItemTheme::G[64], String::new().into(), 0, false) }; - w_exact(20, south!( - w_full(origin_w(east( - button_2("f2", "name ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), - w_full(origin_w(east( - button_2("l", "ength ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), - w_full(origin_w(east( - button_2("r", "epeat ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), - )) - } - pub fn edit_status (&self) -> impl Draw + '_ { - let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.length) - } else { (ItemTheme::G[64], 0) }; - let time_pos = self.get_time_pos(); - let time_zoom = self.get_time_zoom(); - let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; - let note_pos = self.get_note_pos(); - let note_name = format!("{:4}", note_pitch_to_name(note_pos)); - let note_pos = format!("{:>3}", note_pos); - let note_len = format!("{:>4}", self.get_note_len()); - w_exact(20, south!( - w_full(origin_w(east( - button_2("t", "ime ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), - format!("{length} /{time_zoom} +{time_pos} "))))))), - w_full(origin_w(east( - button_2("z", "lock ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), - format!("{time_lock}"))))))), - w_full(origin_w(east( - button_2("x", "note ", false), - w_full(origin_e(Tui::fg(Rgb(255, 255, 255), - format!("{note_name} {note_pos} {note_len}"))))))), - )) - } - } - - impl TimeRange for MidiEditor { - fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } - } - - impl NoteRange for MidiEditor { - fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } - } - - impl NotePoint for MidiEditor { - fn note_len (&self) -> &AtomicUsize { self.mode.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } - } - - impl TimePoint for MidiEditor { - fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } - } - - impl MidiViewer for MidiEditor { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } - fn redraw (&self) { self.mode.redraw() } - fn clip (&self) -> &Option>> { self.mode.clip() } - fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } - fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } - } - - impl MidiEditor { - fn tui (&self) -> impl Draw { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } - } - - - impl PianoHorizontal { - pub fn new (clip: Option<&Arc>>) -> Self { - let size = Measure::new(0, 0); - let mut range = MidiSelection::from((12, true)); - range.time_axis = size.x.clone(); - range.note_axis = size.y.clone(); - let piano = Self { - keys_width: 5, - size, - range, - buffer: RwLock::new(Default::default()).into(), - point: MidiCursor::default(), - clip: clip.cloned(), - color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), - }; - piano.redraw(); - piano - } - } - - impl PianoHorizontal { - fn tui (&self) -> impl Draw { - south( - east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), - east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(self.cursor())))), - ) - } - } - - impl PianoHorizontal { - /// Draw the piano roll background. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { - for (y, note) in (0..=127).rev().enumerate() { - for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { - let cell = buf.get_mut(x, y).unwrap(); - if note == (127-note_point) || time == time_point { - cell.set_bg(Rgb(0,0,0)); - } else { - cell.set_bg(clip.color.darkest.rgb); - } - if time % 384 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('│'); - } else if time % 96 == 0 { - cell.set_fg(clip.color.dark.rgb); - cell.set_char('╎'); - } else if time % note_len == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('┊'); - } else if (127 - note) % 12 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('='); - } else if (127 - note) % 6 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('—'); - } else { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('·'); - } - } - } - } - /// Draw the piano roll foreground. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { - let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); - let mut notes_on = [false;128]; - for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { - for (_y, note) in (0..=127).rev().enumerate() { - if let Some(cell) = buf.get_mut(x, note) { - if notes_on[note] { - cell.set_char('▂'); - cell.set_style(style); - } - } - } - let time_end = time_start + zoom; - for time in time_start..time_end.min(clip.length) { - for event in clip.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - if let Some(cell) = buf.get_mut(x, note) { - cell.set_char('█'); - cell.set_style(style); - } - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } - } - } - - } - } - fn notes (&self) -> impl Draw { - let time_start = self.get_time_start(); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - let buffer = self.buffer.clone(); - Thunk::new(move|to: &mut Tui|{ - let source = buffer.read().unwrap(); - let XYWH(x0, y0, w, _h) = to.area(); - //if h as usize != note_axis { - //panic!("area height mismatch: {h} <> {note_axis}"); - //} - for (area_x, screen_x) in (x0..x0+w).enumerate() { - for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { - let source_x = time_start + area_x; - let source_y = note_hi - area_y; - // TODO: enable loop rollover: - //let source_x = (time_start + area_x) % source.width.max(1); - //let source_y = (note_hi - area_y) % source.height.max(1); - let is_in_x = source_x < source.width; - let is_in_y = source_y < source.height; - if is_in_x && is_in_y { - if let Some(source_cell) = source.get(source_x, source_y) { - if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { - *cell = source_cell.clone(); - } - } - } - } - } - }) - } - fn cursor (&self) -> impl Draw { - let note_hi = self.get_note_hi(); - let note_lo = self.get_note_lo(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let time_pos = self.get_time_pos(); - let time_start = self.get_time_start(); - let time_zoom = self.get_time_zoom(); - let style = Some(Style::default().fg(self.color.lightest.rgb)); - Thunk::new(move|to: &mut Tui|{ - let XYWH(x0, y0, w, _) = to.area(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - if note == note_pos { - for x in 0..w { - let screen_x = x0 + x; - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_pos && time_pos < time_2 { - to.blit(&"█", screen_x, screen_y, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (screen_x + 1)..(screen_x + tail) { - to.blit(&"▂", x_tail, screen_y, style); - } - break - } - } - break - } - } - }) - } - fn keys (&self) -> impl Draw { - let state = self; - let color = state.color; - let note_lo = state.get_note_lo(); - let note_hi = state.get_note_hi(); - let note_pos = state.get_note_pos(); - let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); - let off_style = Some(Style::default().fg(Tui::g(255))); - let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); - h_full(w_exact(self.keys_width, Thunk::new(move|to: &mut Tui|{ - let XYWH(x, y0, _w, _h) = to.area(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - to.blit(&to_key(note), x, screen_y, key_style); - if note > 127 { - continue - } - if note == note_pos { - to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style) - } else { - to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) - }; - } - }))) - } - fn timeline (&self) -> impl Draw + '_ { - w_full(h_exact(1, Thunk::new(move|to: &mut Tui|{ - let XYWH(x, y, w, _h) = to.area(); - let style = Some(Style::default().dim()); - let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { - let t = area_x as usize * self.time_zoom().get(); - if t < length { - to.blit(&"|", screen_x, y, style); - } - } - }))) - } - } - - impl TimeRange for PianoHorizontal { - fn time_len (&self) -> &AtomicUsize { self.range.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.range.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } - } - - impl NoteRange for PianoHorizontal { - fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } - } - - impl NotePoint for PianoHorizontal { - fn note_len (&self) -> &AtomicUsize { self.point.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } - } - - impl TimePoint for PianoHorizontal { - fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } - } - - impl MidiViewer for PianoHorizontal { - fn clip (&self) -> &Option>> { &self.clip } - fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } - /// Determine the required space to render the clip. - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { (clip.length / self.range.time_zoom().get(), 128) } - fn redraw(self) { - *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { - let clip = clip.read().unwrap(); - let buf_size = self.buffer_size(&clip); - let mut buffer = BigBuffer::from(buf_size); - let time_zoom = self.get_time_zoom(); - self.time_len().set(clip.length); - PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); - PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); - buffer - } else { - Default::default() - } - } - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); - self.redraw(); - } - } - - impl std::fmt::Debug for PianoHorizontal { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - let buffer = self.buffer.read().unwrap(); - f.debug_struct("PianoHorizontal") - .field("time_zoom", &self.range.time_zoom) - .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) - .finish() - } - } - impl OctaveVertical { - fn color (&self, pitch: usize) -> Color { - let pitch = pitch % 12; - self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] - } - } - impl OctaveVertical { - fn tui (&self) -> impl Draw { - east!( - Tui::fg_bg(self.color(0), self.color(1), "▙"), - Tui::fg_bg(self.color(2), self.color(3), "▙"), - Tui::fg_bg(self.color(4), self.color(5), "▌"), - Tui::fg_bg(self.color(6), self.color(7), "▟"), - Tui::fg_bg(self.color(8), self.color(9), "▟"), - Tui::fg_bg(self.color(10), self.color(11), "▟"), - ) - } - } -} - -#[cfg(feature = "sampler")] mod sampler { - use crate::*; - impl Default for SampleKit { - fn default () -> Self { Self([const { None }; N]) } - } - impl Iterator for Voice { - type Item = [f32;2]; - fn next (&mut self) -> Option { - if self.after > 0 { - self.after -= 1; - return Some([0.0, 0.0]) - } - let sample = self.sample.read().unwrap(); - if self.position < sample.end { - let position = self.position; - self.position += 1; - return sample.channels[0].get(position).map(|_amplitude|[ - sample.channels[0][position] * self.velocity * sample.gain, - sample.channels[0][position] * self.velocity * sample.gain, - ]) - } - None - } - } - impl NoteRange for Sampler { - fn note_lo (&self) -> &AtomicUsize { - &self.note_lo - } - fn note_axis (&self) -> &AtomicUsize { - &self.size.y - } - } - impl NotePoint for Sampler { - fn note_len (&self) -> &AtomicUsize { - unreachable!(); - } - fn get_note_len (&self) -> usize { - 0 - } - fn set_note_len (&self, _x: usize) -> usize { - 0 /*TODO?*/ - } - fn note_pos (&self) -> &AtomicUsize { - &self.note_pt - } - fn get_note_pos (&self) -> usize { - self.note_pt.load(Relaxed) - } - fn set_note_pos (&self, x: usize) -> usize { - let old = self.note_pt.swap(x, Relaxed); - self.cursor.0.store(x % 8, Relaxed); - self.cursor.1.store(x / 8, Relaxed); - old - } - } - impl Sampler { - pub fn new ( - jack: &Jack<'static>, - name: impl AsRef, - #[cfg(feature = "port")] midi_from: &[Connect], - #[cfg(feature = "port")] audio_from: &[&[Connect];2], - #[cfg(feature = "port")] audio_to: &[&[Connect];2], - ) -> Usually { - let name = name.as_ref(); - Ok(Self { - name: name.into(), - input_meters: vec![0.0;2], - output_meters: vec![0.0;2], - output_gain: 1., - buffer: vec![vec![0.0;16384];2], - #[cfg(feature = "port")] midi_in: Some( - MidiInput::new(jack, &format!("M/{name}"), midi_from)? - ), - #[cfg(feature = "port")] audio_ins: vec![ - AudioInput::new(jack, &format!("L/{name}"), audio_from[0])?, - AudioInput::new(jack, &format!("R/{name}"), audio_from[1])?, - ], - #[cfg(feature = "port")] audio_outs: vec![ - AudioOutput::new(jack, &format!("{name}/L"), audio_to[0])?, - AudioOutput::new(jack, &format!("{name}/R"), audio_to[1])?, - ], - ..Default::default() - }) - } - /// Value of cursor - pub fn cursor (&self) -> (usize, usize) { - (self.cursor.0.load(Relaxed), self.cursor.1.load(Relaxed)) - } - fn sample_selected (&self) -> usize { - (self.get_note_pos() as u8).into() - } - fn sample_selected_pitch (&self) -> u7 { - (self.get_note_pos() as u8).into() - } - pub fn process_audio_in (&mut self, scope: &ProcessScope) { - self.reset_input_meters(); - if self.recording.is_some() { - self.record_into(scope); - } else { - self.update_input_meters(scope); - } - } - /// Make sure that input meter count corresponds to input channel count - fn reset_input_meters (&mut self) { - let channels = self.audio_ins.len(); - if self.input_meters.len() != channels { - self.input_meters = vec![f32::MIN;channels]; - } - } - /// Record from inputs to sample - fn record_into (&mut self, scope: &ProcessScope) { - if let Some(ref sample) = self.recording.as_ref().expect("no recording sample").1 { - let mut sample = sample.write().unwrap(); - if sample.channels.len() != self.audio_ins.len() { - panic!("channel count mismatch"); - } - let samples_with_meters = self.audio_ins.iter() - .zip(self.input_meters.iter_mut()) - .zip(sample.channels.iter_mut()); - let mut length = 0; - for ((input, meter), channel) in samples_with_meters { - let slice = input.port().as_slice(scope); - length = length.max(slice.len()); - *meter = to_rms(slice); - channel.extend_from_slice(slice); - } - sample.end += length; - } else { - panic!("tried to record into the void") - } - } - /// Update input meters - fn update_input_meters (&mut self, scope: &ProcessScope) { - for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { - let slice = input.port().as_slice(scope); - *meter = to_rms(slice); - } - } - /// Make sure that output meter count corresponds to input channel count - fn reset_output_meters (&mut self) { - let channels = self.audio_outs.len(); - if self.output_meters.len() != channels { - self.output_meters = vec![f32::MIN;channels]; - } - } - /// Mix all currently playing samples into the output. - pub fn process_audio_out (&mut self, scope: &ProcessScope) { - self.clear_output_buffer(); - self.populate_output_buffer(scope.n_frames() as usize); - self.write_output_buffer(scope); - } - /// Zero the output buffer. - fn clear_output_buffer (&mut self) { - for buffer in self.buffer.iter_mut() { - buffer.fill(0.0); - } - } - /// Write playing voices to output buffer - fn populate_output_buffer (&mut self, frames: usize) { - let Sampler { buffer, voices, output_gain, mixing_mode, .. } = self; - let _channel_count = buffer.len(); - match mixing_mode { - MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{ - mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) - }), - MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{ - mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) - }), - } - } - /// Write output buffer to output ports. - fn write_output_buffer (&mut self, scope: &ProcessScope) { - let Sampler { audio_outs, buffer, .. } = self; - for (i, port) in audio_outs.iter_mut().enumerate() { - let buffer = &buffer[i]; - for (i, value) in port.port_mut().as_mut_slice(scope).iter_mut().enumerate() { - *value = *buffer.get(i).unwrap_or(&0.0); - } - } - } - } - impl SampleAdd { - fn exited (&self) -> bool { - self.exited - } - fn exit (&mut self) { - self.exited = true - } - pub fn new ( - sample: &Arc>, - voices: &Arc>> - ) -> Usually { - let dir = std::env::current_dir()?; - let (subdirs, files) = scan(&dir)?; - Ok(Self { - exited: false, - dir, - subdirs, - files, - cursor: 0, - offset: 0, - sample: sample.clone(), - voices: voices.clone(), - _search: None - }) - } - fn rescan (&mut self) -> Usually<()> { - scan(&self.dir).map(|(subdirs, files)|{ - self.subdirs = subdirs; - self.files = files; - }) - } - fn prev (&mut self) { - self.cursor = self.cursor.saturating_sub(1); - } - fn next (&mut self) { - self.cursor = self.cursor + 1; - } - fn try_preview (&mut self) -> Usually<()> { - if let Some(path) = self.cursor_file() { - if let Ok(sample) = Sample::from_file(&path) { - *self.sample.write().unwrap() = sample; - self.voices.write().unwrap().push( - Sample::play(&self.sample, 0, &u7::from(100u8)) - ); - } - //load_sample(&path)?; - //let src = std::fs::File::open(&path)?; - //let mss = MediaSourceStream::new(Box::new(src), Default::default()); - //let mut hint = Hint::new(); - //if let Some(ext) = path.extension() { - //hint.with_extension(&ext.to_string_lossy()); - //} - //let meta_opts: MetadataOptions = Default::default(); - //let fmt_opts: FormatOptions = Default::default(); - //if let Ok(mut probed) = symphonia::default::get_probe() - //.format(&hint, mss, &fmt_opts, &meta_opts) - //{ - //panic!("{:?}", probed.format.metadata()); - //}; - } - Ok(()) - } - fn cursor_dir (&self) -> Option { - if self.cursor < self.subdirs.len() { - Some(self.dir.join(&self.subdirs[self.cursor])) - } else { - None - } - } - fn cursor_file (&self) -> Option { - if self.cursor < self.subdirs.len() { - return None - } - let index = self.cursor.saturating_sub(self.subdirs.len()); - if index < self.files.len() { - Some(self.dir.join(&self.files[index])) - } else { - None - } - } - fn pick (&mut self) -> Usually { - if self.cursor == 0 { - if let Some(parent) = self.dir.parent() { - self.dir = parent.into(); - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - } - if let Some(dir) = self.cursor_dir() { - self.dir = dir; - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - if let Some(path) = self.cursor_file() { - let (end, channels) = read_sample_data(&path.to_string_lossy())?; - let mut sample = self.sample.write().unwrap(); - sample.name = path.file_name().unwrap().to_string_lossy().into(); - sample.end = end; - sample.channels = channels; - return Ok(true) - } - return Ok(false) - } - } - impl SampleKit { - pub fn get (&self, index: usize) -> &Option>> { - if index < self.0.len() { - &self.0[index] - } else { - &None - } - } - } - impl Sample { - pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { - Self { - name: name.as_ref().into(), - start, - end, - channels, - rate: None, - gain: 1.0, - color: ItemTheme::random(), - } - } - pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { - Voice { - sample: sample.clone(), - after, - position: sample.read().unwrap().start, - velocity: velocity.as_int() as f32 / 127.0, - } - } - pub fn handle_cc (&mut self, controller: u7, value: u7) { - let percentage = value.as_int() as f64 / 127.; - match controller.as_int() { - 20 => { - self.start = (percentage * self.end as f64) as usize; - }, - 21 => { - let length = self.channels[0].len(); - self.end = length.min( - self.start + (percentage * (length as f64 - self.start as f64)) as usize - ); - }, - 22 => { /*attack*/ }, - 23 => { /*decay*/ }, - 24 => { - self.gain = percentage as f32 * 2.0; - }, - 26 => { /* pan */ } - 25 => { /* pitch */ } - _ => {} - } - } - /// Read WAV from file - pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { - let mut channels: Vec> = vec![]; - for channel in wavers::Wav::from_path(src)?.channels() { - channels.push(channel); - } - let mut end = 0; - let mut data: Vec> = vec![]; - for samples in channels.iter() { - let channel = Vec::from(samples.as_ref()); - end = end.max(channel.len()); - data.push(channel); - } - Ok((end, data)) - } - pub fn from_file (path: &PathBuf) -> Usually { - let name = path.file_name().unwrap().to_string_lossy().into(); - let mut sample = Self { name, ..Default::default() }; - // Use file extension if present - let mut hint = Hint::new(); - if let Some(ext) = path.extension() { - hint.with_extension(&ext.to_string_lossy()); - } - let probed = symphonia::default::get_probe().format( - &hint, - MediaSourceStream::new( - Box::new(File::open(path)?), - Default::default(), - ), - &Default::default(), - &Default::default() - )?; - let mut format = probed.format; - let params = &format.tracks().iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .expect("no tracks found") - .codec_params; - let mut decoder = get_codecs().make(params, &Default::default())?; - loop { - match format.next_packet() { - Ok(packet) => sample.decode_packet(&mut decoder, packet)?, - Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), - Err(err) => return Err(err.into()), - }; - }; - sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); - Ok(sample) - } - fn decode_packet ( - &mut self, decoder: &mut Box, packet: Packet - ) -> Usually<()> { - // Decode a packet - let decoded = decoder - .decode(&packet) - .map_err(|e|Box::::from(e))?; - // Determine sample rate - let spec = *decoded.spec(); - if let Some(rate) = self.rate { - if rate != spec.rate as usize { - panic!("sample rate changed"); - } - } else { - self.rate = Some(spec.rate as usize); - } - // Determine channel count - while self.channels.len() < spec.channels.count() { - self.channels.push(vec![]); - } - // Load sample - let mut samples = SampleBuffer::new( - decoded.frames() as u64, - spec - ); - if samples.capacity() > 0 { - samples.copy_interleaved_ref(decoded); - for frame in samples.samples().chunks(spec.channels.count()) { - for (chan, frame) in frame.iter().enumerate() { - self.channels[chan].push(*frame) - } - } - } - Ok(()) - } - } - impl Draw for SampleAdd { - fn draw (self, _to: &mut Tui) -> Usually> { - todo!() - } - } - - fn draw_list_item (sample: &Option>>) -> String { - if let Some(sample) = sample { - let sample = sample.read().unwrap(); - format!("{:8}", sample.name) - //format!("{:8} {:3} {:6}-{:6}/{:6}", - //sample.name, - //sample.gain, - //sample.start, - //sample.end, - //sample.channels[0].len() - //) - } else { - String::from("........") - } - } - - fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + use<'_> { - let min_db = -64.0; - Thunk::new(move|to: &mut Tui|{ - let XYWH(x, y, width, height) = to.area(); - let area = Rect { x, y, width, height }; - if let Some(sample) = &sample { - let sample = sample.read().unwrap(); - let start = sample.start as f64; - let end = sample.end as f64; - let length = end - start; - let step = length / width as f64; - let mut t = start; - let mut lines = vec![]; - while t < end { - let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; - let total: f32 = chunk.iter().map(|x|x.abs()).sum(); - let count = chunk.len() as f32; - let meter = 10. * (total / count).log10(); - let x = t as f64; - let y = meter as f64; - lines.push(Line::new(x, min_db, x, y, Color::Green)); - t += step / 2.; - } - Canvas::default() - .x_bounds([sample.start as f64, sample.end as f64]) - .y_bounds([min_db, 0.]) - .paint(|ctx| { - for line in lines.iter() { - ctx.draw(line); - } - //FIXME: proportions - //let text = "press record to finish sampling"; - //ctx.print( - //(width - text.len() as u16) as f64 / 2.0, - //height as f64 / 2.0, - //text.red() - //); - }).render(area, &mut to.buffer); - } else { - Canvas::default() - .x_bounds([0.0, width as f64]) - .y_bounds([0.0, height as f64]) - .paint(|_ctx| { - //let text = "press record to begin sampling"; - //ctx.print( - //(width - text.len() as u16) as f64 / 2.0, - //height as f64 / 2.0, - //text.red() - //); - }) - .render(area, &mut to.buffer); - } - }) - } - - impl_audio!(Sampler: sampler_jack_process); - pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control { - if let Some(midi_in) = &state.midi_in { - for midi in midi_in.port().iter(scope) { - sampler_midi_in(&state.samples, &state.voices, midi) - } - } - state.process_audio_out(scope); - state.process_audio_in(scope); - Control::Continue - } - - /// Create [Voice]s from [Sample]s in response to MIDI input. - fn sampler_midi_in ( - samples: &SampleKit<128>, voices: &Arc>>, RawMidi { time, bytes }: RawMidi - ) { - if let Ok(LiveEvent::Midi { message, .. }) = LiveEvent::parse(bytes) { - match message { - MidiMessage::NoteOn { ref key, ref vel } => { - if let Some(sample) = samples.get(key.as_int() as usize) { - voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); - } - }, - MidiMessage::Controller { controller: _, value: _ } => { - // TODO - } - _ => {} - } - } - } - - fn draw_sample ( - to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool - ) -> Usually { - let style = if focus { Style::default().green() } else { Style::default() }; - if focus { - to.blit(&"🬴", x+1, y, Some(style.bold())); - } - let label1 = format!("{:3} {:12}", - note.map(|n|n.to_string()).unwrap_or(String::default()), - sample.name); - let label2 = format!("{:>6} {:>6} +0.0", - sample.start, - sample.end); - to.blit(&label1, x+2, y, Some(style.bold())); - to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); - Ok(label1.len() + label2.len() + 4) - } -} - -#[cfg(feature = "lv2")] mod lv2 { - use crate::*; - impl_audio!(Lv2: lv2_jack_process); - impl Lv2 { - const INPUT_BUFFER: usize = 1024; - pub fn new ( - jack: &Jack<'static>, - name: &str, - uri: &str, - ) -> Usually { - let lv2_world = livi::World::with_load_bundle(&uri); - let lv2_features = lv2_world.build_features(livi::FeaturesBuilder { - min_block_length: 1, - max_block_length: 65536, - }); - let lv2_plugin = lv2_world.iter_plugins().nth(0) - .unwrap_or_else(||panic!("plugin not found: {uri}")); - Ok(Self { - jack: jack.clone(), - name: name.into(), - path: Some(String::from(uri).into()), - selected: 0, - mapping: false, - midi_ins: vec![], - midi_outs: vec![], - audio_ins: vec![], - audio_outs: vec![], - lv2_instance: unsafe { - lv2_plugin - .instantiate(lv2_features.clone(), 48000.0) - .expect(&format!("instantiate failed: {uri}")) - }, - lv2_port_list: lv2_plugin.ports().collect::>(), - lv2_input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), - lv2_ui_thread: None, - lv2_world, - lv2_features, - lv2_plugin, - }) - } - } - fn lv2_jack_process ( - Lv2 { - midi_ins, midi_outs, audio_ins, audio_outs, - lv2_features, lv2_instance, lv2_input_buffer, .. - }: &mut Lv2, - _client: &Client, - scope: &ProcessScope - ) -> Control { - let urid = lv2_features.midi_urid(); - lv2_input_buffer.clear(); - for port in midi_ins.iter() { - let mut atom = ::livi::event::LV2AtomSequence::new( - &lv2_features, - scope.n_frames() as usize - ); - for event in port.iter(scope) { - match event.bytes.len() { - 3 => atom.push_midi_event::<3>( - event.time as i64, - urid, - &event.bytes[0..3] - ).unwrap(), - _ => {} - } - } - lv2_input_buffer.push(atom); - } - let mut outputs = vec![]; - for _ in midi_outs.iter() { - outputs.push(::livi::event::LV2AtomSequence::new( - lv2_features, - scope.n_frames() as usize - )); - } - let ports = ::livi::EmptyPortConnections::new() - .with_atom_sequence_inputs(lv2_input_buffer.iter()) - .with_atom_sequence_outputs(outputs.iter_mut()) - .with_audio_inputs(audio_ins.iter().map(|o|o.as_slice(scope))) - .with_audio_outputs(audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); - unsafe { - lv2_instance.run(scope.n_frames() as usize, ports).unwrap() - }; - Control::Continue - } - - impl LV2PluginUI { pub fn new () -> Usually { Ok(Self { window: None }) } } - - impl ApplicationHandler for LV2PluginUI { - fn resumed (&mut self, event_loop: &ActiveEventLoop) { - self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); - } - fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { - match event { - WindowEvent::CloseRequested => { - self.window.as_ref().unwrap().set_visible(false); - event_loop.exit(); - }, - WindowEvent::RedrawRequested => { - self.window.as_ref().unwrap().request_redraw(); - } - _ => (), - } - } - } - - impl Draw for Lv2 { - fn draw(self, to: &mut Tui) { - let area = to.area(); - let XYWH(x, y, _, height) = area; - let mut width = 20u16; - let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1)); - let end = start + height as usize - 2; - //draw_box(buf, Rect { x, y, width, height }); - for i in start..end { - if let Some(port) = self.lv2_port_list.get(i) { - let value = if let Some(value) = self.lv2_instance.control_input(port.index) { - value - } else { - port.default_value - }; - //let label = &format!("C·· M·· {:25} = {value:.03}", port.name); - let label = &format!("{:25} = {value:.03}", port.name); - width = width.max(label.len() as u16 + 4); - let style = if i == self.selected { - Some(Style::default().green()) - } else { - None - } ; - to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style); - } else { - break - } - } - draw_header(self, to, x, y, width); - } - } - - - fn draw_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) { - let style = Style::default().gray(); - let label1 = format!(" {}", state.name); - to.blit(&label1, x + 1, y, Some(style.white().bold())); - if let Some(ref path) = state.path { - let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]); - to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim())); - } - //Ok(Rect { x, y, width: w, height: 1 }) - } -} - -mod pool { - use crate::*; - has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); - impl_has_clips!(|self: Pool|self.clips); - impl_from!(Pool: |clip:&Arc>|{ - let model = Self::default(); - model.clips.write().unwrap().push(clip.clone()); - model.clip.store(1, Relaxed); - model - }); - impl_default!(Pool: Self { - browse: None, - clip: 0.into(), - clips: Arc::from(RwLock::from(vec![])), - mode: None, - samples: Arc::from(RwLock::from(vec![])), - visible: true, - }); - impl Pool { - pub fn clip_index (&self) -> usize { - self.clip.load(Relaxed) - } - pub fn set_clip_index (&self, value: usize) { - self.clip.store(value, Relaxed); - } - pub fn mode (&self) -> &Option { - &self.mode - } - pub fn mode_mut (&mut self) -> &mut Option { - &mut self.mode - } - pub fn begin_clip_length (&mut self) { - let length = self.clips()[self.clip_index()].read().unwrap().length; - *self.mode_mut() = Some(PoolMode::Length( - self.clip_index(), - length, - ClipLengthFocus::Bar - )); - } - pub fn begin_clip_rename (&mut self) { - let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); - *self.mode_mut() = Some(PoolMode::Rename( - self.clip_index(), - name - )); - } - pub fn begin_import (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Import( - self.clip_index(), - Browse::new(None)? - )); - Ok(()) - } - pub fn begin_export (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Export( - self.clip_index(), - Browse::new(None)? - )); - Ok(()) - } - pub fn new_clip (&self) -> MidiClip { - MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) - } - pub fn cloned_clip (&self) -> MidiClip { - let index = self.clip_index(); - let mut clip = self.clips()[index].read().unwrap().duplicate(); - clip.color = ItemTheme::random_near(clip.color, 0.25); - clip - } - pub fn add_new_clip (&self) -> (usize, Arc>) { - let clip = Arc::new(RwLock::new(self.new_clip())); - let index = { - let mut clips = self.clips.write().unwrap(); - clips.push(clip.clone()); - clips.len().saturating_sub(1) - }; - self.clip.store(index, Relaxed); - (index, clip) - } - pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { - let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); - if let Some(index) = index { - self.clips.write().unwrap().remove(index); - return true - } - false - } - } - impl ClipLengthFocus { - pub fn next (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } - } - pub fn prev (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } - } - } - impl ClipLength { - pub fn _new (pulses: usize, focus: Option) -> Self { - Self { ppq: PPQ, bpb: 4, pulses, focus } - } - pub fn bars (&self) -> usize { - self.pulses / (self.bpb * self.ppq) - } - pub fn beats (&self) -> usize { - (self.pulses % (self.bpb * self.ppq)) / self.ppq - } - pub fn ticks (&self) -> usize { - self.pulses % self.ppq - } - pub fn bars_string (&self) -> Arc { - format!("{}", self.bars()).into() - } - pub fn beats_string (&self) -> Arc { - format!("{}", self.beats()).into() - } - pub fn ticks_string (&self) -> Arc { - format!("{:>02}", self.ticks()).into() - } - } - - impl Pool { - fn _todo_usize_ (&self) -> usize { todo!() } - fn _todo_bool_ (&self) -> bool { todo!() } - fn _todo_clip_ (&self) -> MidiClip { todo!() } - fn _todo_path_ (&self) -> PathBuf { todo!() } - fn _todo_color_ (&self) -> ItemColor { todo!() } - fn _todo_str_ (&self) -> Arc { todo!() } - fn _clip_new (&self) -> MidiClip { self.new_clip() } - fn _clip_cloned (&self) -> MidiClip { self.cloned_clip() } - fn _clip_index_current (&self) -> usize { 0 } - fn _clip_index_after (&self) -> usize { 0 } - fn _clip_index_previous (&self) -> usize { 0 } - fn _clip_index_next (&self) -> usize { 0 } - fn _color_random (&self) -> ItemColor { ItemColor::random() } - } - - impl<'a> PoolView<'a> { - fn tui (&self) -> impl Draw { - let Self(pool) = self; - //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); - //let on_bg = |x|x;//below(Repeat(" "), Tui::bg(color.darkest.rgb, x)); - //let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); - //let height = pool.clips.read().unwrap().len() as u16; - w_exact(20, h_full(origin_n(iter( - ||pool.clips().clone().into_iter(), - move|clip: Arc>, i: usize|{ - let item_height = 1; - let item_offset = i as u16 * item_height; - let selected = i == pool.clip_index(); - let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); - let bg = if selected { color.light.rgb } else { color.base.rgb }; - let fg = color.lightest.rgb; - let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; - let length = if false { String::default() } else { format!("{length} ") }; - h_exact(1, iter_south(item_offset, item_height, Tui::bg(bg, below!( - w_full(origin_w(Tui::fg(fg, Tui::bold(selected, name)))), - w_full(origin_e(Tui::fg(fg, Tui::bold(selected, length)))), - w_full(origin_w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), - w_full(origin_e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), - )))) - })))) - } - } - impl ClipLength { - fn tui (&self) -> impl Draw { - use ClipLengthFocus::*; - let bars = ||self.bars_string(); - let beats = ||self.beats_string(); - let ticks = ||self.ticks_string(); - match self.focus { - None => east!(" ", bars(), ".", beats(), ".", ticks()), - Some(Bar) => east!("[", bars(), "]", beats(), ".", ticks()), - Some(Beat) => east!(" ", bars(), "[", beats(), "]", ticks()), - Some(Tick) => east!(" ", bars(), ".", beats(), "[", ticks()), - } - } - } -} - -mod config { - use crate::*; -} - -mod dialog { - use crate::*; - - impl Dialog { - /// ``` - /// let _ = tek::Dialog::welcome(); - /// ``` - pub fn welcome () -> Self { - Self::Menu(1, MenuItems([ - MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), - MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ - app.dialog = Dialog::None; - app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); - })))), - MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), - ].into())) - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().menu_selected(); - /// ``` - pub fn menu_selected (&self) -> Option { - if let Self::Menu(selected, _) = self { Some(*selected) } else { None } - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().menu_next(); - /// ``` - pub fn menu_next (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), - _ => Self::None - } - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().menu_prev(); - /// ``` - pub fn menu_prev (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), - _ => Self::None - } - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().device_kind(); - /// ``` - pub fn device_kind (&self) -> Option { - if let Self::Device(index) = self { Some(*index) } else { None } - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().device_kind_next(); - /// ``` - pub fn device_kind_next (&self) -> Option { - self.device_kind().map(|index|(index + 1) % device_kinds().len()) - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().device_kind_prev(); - /// ``` - pub fn device_kind_prev (&self) -> Option { - self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) - } - /// FIXME: implement - pub fn message (&self) -> Option<&str> { todo!() } - /// FIXME: implement - pub fn browser (&self) -> Option<&Arc> { todo!() } - /// FIXME: implement - pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } - } -} - -impl_audio!(|self: DeviceAudio<'a>, client, scope|{ - use Device::*; - match self.0 { - Mute => { Control::Continue }, - Bypass => { /*TODO*/ Control::Continue }, - #[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope), - #[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope), - #[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO - #[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO - #[cfg(feature = "clap")] Clap => { todo!() }, // TODO - #[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO - } -}); diff --git a/src/tick.rs b/src/tick.rs deleted file mode 100644 index c6bdd169..00000000 --- a/src/tick.rs +++ /dev/null @@ -1,168 +0,0 @@ -pub trait HasClock: AsRef + AsMut { - fn clock (&self) -> &Clock { self.as_ref() } - fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } -} - - -/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) -/// -/// ``` -/// let _ = tek::Timebase::default(); -/// ``` -#[derive(Debug, Clone)] pub struct Timebase { - /// Audio samples per second - pub sr: SampleRate, - /// MIDI beats per minute - pub bpm: Bpm, - /// MIDI ticks per beat - pub ppq: Ppq, -} - -/// Iterator that emits subsequent ticks within a range. -/// -/// ``` -/// let iter = tek::Ticker::default(); -/// ``` -#[derive(Debug, Default)] pub struct Ticker { - pub spp: f64, - pub sample: usize, - pub start: usize, - pub end: usize, -} - -/// A point in time in all time scales (microsecond, sample, MIDI pulse) -/// -/// ``` -/// let _ = tek::Moment::default(); -/// ``` -#[derive(Debug, Default, Clone)] pub struct Moment { - pub timebase: Arc, - /// Current time in microseconds - pub usec: Microsecond, - /// Current time in audio samples - pub sample: SampleCount, - /// Current time in MIDI pulses - pub pulse: Pulse, -} - -/// -/// ``` -/// let _ = tek::Moment2::default(); -/// ``` -#[derive(Debug, Clone, Default)] pub enum Moment2 { - #[default] None, - Zero, - Usec(Microsecond), - Sample(SampleCount), - Pulse(Pulse), -} - -/// MIDI resolution in PPQ (pulses per quarter note) -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64); - -/// Timestamp in MIDI pulses -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64); - -/// Tempo in beats per minute -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64); - -/// Quantization setting for launching clips -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64); - -/// Quantization setting for notes -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64); - -/// Timestamp in audio samples -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64); - -/// Audio sample rate in Hz (samples per second) -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64); - -/// Timestamp in microseconds -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64); - -/// The source of time. -/// -/// ``` -/// let clock = tek::Clock::default(); -/// ``` -#[derive(Clone, Default)] pub struct Clock { - /// JACK transport handle. - pub transport: Arc>, - /// Global temporal resolution (shared by [Moment] fields) - pub timebase: Arc, - /// Current global sample and usec (monotonic from JACK clock) - pub global: Arc, - /// Global sample and usec at which playback started - pub started: Arc>>, - /// Playback offset (when playing not from start) - pub offset: Arc, - /// Current playhead position - pub playhead: Arc, - /// Note quantization factor - pub quant: Arc, - /// Launch quantization factor - pub sync: Arc, - /// Size of buffer in samples - pub chunk: Arc, - // Cache of formatted strings - pub view_cache: Arc>, - /// For syncing the clock to an external source - #[cfg(feature = "port")] pub midi_in: Arc>>, - /// For syncing other devices to this clock - #[cfg(feature = "port")] pub midi_out: Arc>>, - /// For emitting a metronome - #[cfg(feature = "port")] pub click_out: Arc>>, -} - -/// A unit of time, represented as an atomic 64-bit float. -/// -/// According to https://stackoverflow.com/a/873367, as per IEEE754, -/// every integer between 1 and 2^53 can be represented exactly. -/// This should mean that, even at 192kHz sampling rate, over 1 year of audio -/// can be clocked in microseconds with f64 without losing precision. -pub trait TimeUnit: InteriorMutable {} - -/// Contains memoized renders of clock values. -/// -/// Performance optimization. -#[derive(Debug)] pub struct ClockView { - pub sr: Memo, String>, - pub buf: Memo, String>, - pub lat: Memo, String>, - pub bpm: Memo, String>, - pub beat: Memo, String>, - pub time: Memo, String>, -}