From 9f70441627e8967be64d03f4dc7a5f959264cffa Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 24 Apr 2025 19:33:22 +0300 Subject: [PATCH] refactor sampler, flatten arranger --- crates/app/src/api.rs | 56 +- crates/app/src/lib.rs | 70 ++ crates/app/src/model.rs | 253 ++++- crates/app/src/model/model_scene.rs | 97 -- crates/app/src/model/model_select.rs | 51 - crates/app/src/model/model_track.rs | 103 -- crates/app/src/view.rs | 509 +++++++++- crates/app/src/view/view_clock.rs | 88 -- crates/app/src/view/view_layout.rs | 178 ---- crates/app/src/view/view_memo.rs | 120 --- crates/app/src/view/view_meter.rs | 48 - crates/app/src/view/view_ports.rs | 140 --- crates/cli/edn/sampler.edn | 3 + crates/cli/tek.rs | 7 +- .../keys_sampler.edn => edn/sampler_keys.edn} | 0 crates/sampler/edn/sampler_view.edn | 0 crates/sampler/sampler_scratch.rs | 93 ++ crates/sampler/src/has_sampler.rs | 30 - crates/sampler/src/lib.rs | 14 +- crates/sampler/src/sampler.rs | 901 ------------------ crates/sampler/src/sampler_api.rs | 119 +++ crates/sampler/src/sampler_audio.rs | 107 +++ crates/sampler/src/sampler_browse.rs | 181 ++++ crates/sampler/src/sampler_data.rs | 87 ++ crates/sampler/src/sampler_midi.rs | 54 ++ crates/sampler/src/sampler_model.rs | 149 +++ crates/sampler/src/sampler_view.rs | 192 ++++ deps/tengri | 2 +- 28 files changed, 1816 insertions(+), 1836 deletions(-) delete mode 100644 crates/app/src/model/model_scene.rs delete mode 100644 crates/app/src/model/model_select.rs delete mode 100644 crates/app/src/model/model_track.rs delete mode 100644 crates/app/src/view/view_clock.rs delete mode 100644 crates/app/src/view/view_layout.rs delete mode 100644 crates/app/src/view/view_memo.rs delete mode 100644 crates/app/src/view/view_meter.rs delete mode 100644 crates/app/src/view/view_ports.rs create mode 100644 crates/cli/edn/sampler.edn rename crates/sampler/{src/keys_sampler.edn => edn/sampler_keys.edn} (100%) create mode 100644 crates/sampler/edn/sampler_view.edn create mode 100644 crates/sampler/sampler_scratch.rs delete mode 100644 crates/sampler/src/has_sampler.rs delete mode 100644 crates/sampler/src/sampler.rs create mode 100644 crates/sampler/src/sampler_api.rs create mode 100644 crates/sampler/src/sampler_audio.rs create mode 100644 crates/sampler/src/sampler_browse.rs create mode 100644 crates/sampler/src/sampler_data.rs create mode 100644 crates/sampler/src/sampler_midi.rs create mode 100644 crates/sampler/src/sampler_model.rs create mode 100644 crates/sampler/src/sampler_view.rs diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index 95486435..dba69d6a 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -14,6 +14,7 @@ view!(TuiOut: |self: Tek| self.size.of(View(self, self.view)); { .boxed(), ":sample" => ().boxed(),//self.view_sample(self.is_editing()).boxed(), ":sampler" => ().boxed(),//self.view_sampler(self.is_editing(), &self.editor).boxed(), + //":samples-grid" => SamplerView::new(self).boxed(), ":status" => self.view_status().boxed(), ":pool" => self.pool.as_ref() .map(|pool|Fixed::x(self.w_sidebar(), PoolView(self.is_editing(), pool))) @@ -85,19 +86,6 @@ expose!([self: Tek] { } }); -macro_rules! defcom { - (|$self:ident, $app:ident:$App:ty| $($Command:ident { $( - $Variant:ident $(($($param:ident: $Param:ty),+))? => $expr:expr - )* $(,)? })*) => { - $(#[derive(Clone, Debug)] pub enum $Command { - $($Variant $(($($Param),+))?),* - })* - $(command!(|$self: $Command, $app: $App|match $self { - $($Command::$Variant $(($($param),+))? => $expr),* - });)* - } -} - defcom! { |self, app: Tek| TekCommand { @@ -201,30 +189,26 @@ defcom! { |self, app: Tek| app.editing.store(!app.is_editing(), Relaxed); }; // autocreate: create new clip from pool when entering empty cell - if let Some(ref pool) = app.pool { - if app.is_editing() { - if let Selection::Clip(t, s) = app.selected { - if let Some(scene) = app.scenes.get_mut(s) { - if let Some(slot) = scene.clips.get_mut(t) { - if slot.is_none() { - let (index, mut clip) = pool.add_new_clip(); - // autocolor: new clip colors from scene and track color - clip.write().unwrap().color = ItemColor::random_near( - app.tracks[t].color.base.mix( - scene.color.base, - 0.5 - ), - 0.2 - ).into(); - if let Some(ref mut editor) = app.editor { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip); - } - } - } - } + if let Some(ref pool) = app.pool + && app.is_editing() + && let Selection::Clip(t, s) = app.selected + && let Some(scene) = app.scenes.get_mut(s) + && let Some(slot) = scene.clips.get_mut(t) + && slot.is_none() + { + let (index, mut clip) = pool.add_new_clip(); + // autocolor: new clip colors from scene and track color + clip.write().unwrap().color = ItemColor::random_near( + app.tracks[t].color.base.mix( + scene.color.base, + 0.5 + ), + 0.2 + ).into(); + if let Some(ref mut editor) = app.editor { + editor.set_clip(Some(&clip)); } + *slot = Some(clip); } None } diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index 16ecd001..fdad3826 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -14,6 +14,7 @@ #![feature(type_alias_impl_trait)] #![feature(trait_alias)] #![feature(type_changing_struct_update)] +#![feature(let_chains)] /// Standard result type. pub type Usually = std::result::Result>; /// Standard optional result type. @@ -47,3 +48,72 @@ mod view; pub use self::view::*; let _ = tek.toggle_loop(); let _ = tek.activate(); } +#[cfg(test)] #[test] fn test_model_scene () { + let mut app = Tek::default(); + let _ = app.scene_longest(); + let _ = app.scene(); + let _ = app.scene_mut(); + let _ = app.scene_add(None, None); + app.scene_del(0); + let scene = Scene::default(); + let _ = scene.pulses(); + let _ = scene.is_playing(&[]); +} +#[cfg(test)] #[test] fn test_view_clock () { + let _ = button_play_pause(true); + let mut app = Tek::default(); + let _ = app.view_transport(); + let _ = app.view_status(); + let _ = app.update_clock(); +} +#[cfg(test)] #[test] fn test_view_layout () { + let _ = button_2("", "", true); + let _ = button_2("", "", false); + let _ = button_3("", "", "", true); + let _ = button_3("", "", "", false); + let _ = heading("", "", 0, "", true); + let _ = heading("", "", 0, "", false); + let _ = wrap(Reset, Reset, ""); +} +#[cfg(test)] mod test_view_meter { + use super::*; + use proptest::prelude::*; + #[test] fn test_view_meter () { + let _ = view_meter("", 0.0); + let _ = view_meters(&[0.0, 0.0]); + } + 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(test)] #[test] fn test_view_iter () { + let mut tek = Tek::default(); + tek.editor = Some(Default::default()); + let _: Vec<_> = tek.inputs_with_sizes().collect(); + let _: Vec<_> = tek.outputs_with_sizes().collect(); + let _: Vec<_> = tek.tracks_with_sizes().collect(); + let _: Vec<_> = tek.scenes_with_sizes(true, 10, 10).collect(); + //let _: Vec<_> = tek.scenes_with_colors(true, 10).collect(); + //let _: Vec<_> = tek.scenes_with_track_colors(true, 10, 10).collect(); +} +#[cfg(test)] #[test] fn test_view_sizes () { + let app = Tek::default(); + let _ = app.w(); + let _ = app.w_sidebar(); + let _ = app.w_tracks_area(); + let _ = app.h(); + let _ = app.h_tracks_area(); + let _ = app.h_inputs(); + let _ = app.h_outputs(); + let _ = app.h_scenes(); +} diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index a1607237..6ba1add7 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -1,9 +1,5 @@ use crate::*; -mod model_track; pub use self::model_track::*; -mod model_scene; pub use self::model_scene::*; -mod model_select; pub use self::model_select::*; - #[derive(Default, Debug)] pub struct Tek { /// Must not be dropped for the duration of the process pub jack: Jack, @@ -99,6 +95,83 @@ impl Tek { } Ok(()) } + pub fn tracks_add ( + &mut self, count: usize, width: Option, + midi_from: &[PortConnect], midi_to: &[PortConnect], + ) -> Usually<()> { + let jack = self.jack().clone(); + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..count { + let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); + let mut track = self.track_add(None, Some(color), midi_from, midi_to)?.1; + if let Some(width) = width { + track.width = width; + } + } + Ok(()) + } + pub fn track_add ( + &mut self, name: Option<&str>, color: Option, + midi_froms: &[PortConnect], + midi_tos: &[PortConnect], + ) -> Usually<(usize, &mut Track)> { + let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into()); + let mut track = Track { + width: (name.len() + 2).max(12), + color: color.unwrap_or_else(ItemPalette::random), + player: MidiPlayer::new( + &format!("{name}"), + self.jack(), + Some(self.clock()), + None, + midi_froms, + midi_tos + )?, + 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])) + } + pub fn track_del (&mut self, index: usize) { + self.tracks_mut().remove(index); + for scene in self.scenes_mut().iter_mut() { + scene.clips.remove(index); + } + } + pub fn scenes_add (&mut self, n: usize) -> Usually<()> { + let scene_color_1 = ItemColor::random(); + let scene_color_2 = ItemColor::random(); + for i in 0..n { + let _ = self.scene_add(None, Some( + scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() + ))?; + } + Ok(()) + } + pub fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<(usize, &mut Scene)> + { + let scene = Scene { + name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), + clips: vec![None;self.tracks().len()], + color: color.unwrap_or_else(ItemPalette::random), + }; + self.scenes_mut().push(scene); + let index = self.scenes().len() - 1; + Ok((index, &mut self.scenes_mut()[index])) + } + pub fn scene_default_name (&self) -> Arc { + format!("Sc{:3>}", self.scenes().len() + 1).into() + } } has_size!(|self: Tek|&self.size); @@ -120,7 +193,171 @@ has_editor!(|self: Tek|{ is_editing = self.editing.load(Relaxed); }); -//has_sampler!(|self: Tek|{ - //sampler = self.sampler; - //index = self.editor.as_ref().map(|e|e.note_pos()).unwrap_or(0); -//}); +pub trait HasSelection { + fn selected (&self) -> &Selection; + fn selected_mut (&mut self) -> &mut Selection; +} + +/// Represents the current user selection in the arranger +#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { + /// The whole mix is selected + #[default] Mix, + /// A track is selected. + Track(usize), + /// A scene is selected. + Scene(usize), + /// A clip (track × scene) is selected. + Clip(usize, usize), +} + +/// Focus identification methods +impl Selection { + fn is_mix (&self) -> bool { matches!(self, Self::Mix) } + fn is_track (&self) -> bool { matches!(self, Self::Track(_)) } + fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) } + fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) } + pub fn track (&self) -> Option { + use Selection::*; + match self { Clip(t, _) => Some(*t), Track(t) => Some(*t), _ => None } + } + pub fn scene (&self) -> Option { + use Selection::*; + match self { Clip(_, s) => Some(*s), Scene(s) => Some(*s), _ => None } + } + pub fn describe (&self, tracks: &[Track], scenes: &[Scene]) -> Arc { + format!("{}", match self { + Self::Mix => "Everything".to_string(), + Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)) + .unwrap_or_else(||"T??".into()), + Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)) + .unwrap_or_else(||"S??".into()), + Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { + (Some(_), Some(scene)) => match scene.clip(*t) { + Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), + None => format!("T{t} S{s}: Empty") + }, + _ => format!("T{t} S{s}: Empty"), + } + }).into() + } +} + +impl HasSelection for Tek { + fn selected (&self) -> &Selection { &self.selected } + fn selected_mut (&mut self) -> &mut Selection { &mut self.selected } +} + +pub trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync { + fn midi_ins (&self) -> &Vec; + fn midi_outs (&self) -> &Vec; + fn tracks (&self) -> &Vec; + fn tracks_mut (&mut self) -> &mut Vec; + fn track_longest (&self) -> usize { + self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) + } + const WIDTH_OFFSET: usize = 1; + fn track_next_name (&self) -> Arc { + format!("Track{:02}", self.tracks().len() + 1).into() + } + fn track (&self) -> Option<&Track> { + self.selected().track().and_then(|s|self.tracks().get(s)) + } + fn track_mut (&mut self) -> Option<&mut Track> { + self.selected().track().and_then(|s|self.tracks_mut().get_mut(s)) + } +} + +#[derive(Debug, Default)] pub struct Track { + /// Name of track + pub name: Arc, + /// Preferred width of track column + pub width: usize, + /// Identifying color of track + pub color: ItemPalette, + /// MIDI player state + pub player: MidiPlayer, + /// Device chain + pub devices: Vec>, + /// Inputs of 1st device + pub audio_ins: Vec, + /// Outputs of last device + pub audio_outs: Vec, +} + +has_clock!(|self: Track|self.player.clock); + +has_player!(|self: Track|self.player); + +impl 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 HasTracks for Tek { + fn midi_ins (&self) -> &Vec { &self.midi_ins } + fn midi_outs (&self) -> &Vec { &self.midi_outs } + fn tracks (&self) -> &Vec { &self.tracks } + fn tracks_mut (&mut self) -> &mut Vec { &mut self.tracks } +} + +pub trait HasScenes: HasSelection + HasEditor + Send + Sync { + fn scenes (&self) -> &Vec; + fn scenes_mut (&mut self) -> &mut Vec; + fn scene_longest (&self) -> usize { + self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) + } + fn scene (&self) -> Option<&Scene> { + self.selected().scene().and_then(|s|self.scenes().get(s)) + } + fn scene_mut (&mut self) -> Option<&mut Scene> { + self.selected().scene().and_then(|s|self.scenes_mut().get_mut(s)) + } + fn scene_del (&mut self, index: usize) { + self.selected().scene().and_then(|s|Some(self.scenes_mut().remove(index))); + } +} + +#[derive(Debug, Default)] pub struct Scene { + /// Name of scene + pub name: Arc, + /// Clips in scene, one per track + pub clips: Vec>>>, + /// Identifying color of scene + pub color: ItemPalette, +} + +impl Scene { + /// Returns the pulse length of the longest clip in the scene + 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. + 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.player().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 } + } +} + +impl HasScenes for Tek { + fn scenes (&self) -> &Vec { &self.scenes } + fn scenes_mut (&mut self) -> &mut Vec { &mut self.scenes } +} diff --git a/crates/app/src/model/model_scene.rs b/crates/app/src/model/model_scene.rs deleted file mode 100644 index 8a818a96..00000000 --- a/crates/app/src/model/model_scene.rs +++ /dev/null @@ -1,97 +0,0 @@ -use crate::*; -impl Tek { - pub fn scenes_add (&mut self, n: usize) -> Usually<()> { - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..n { - let _ = self.scene_add(None, Some( - scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() - ))?; - } - Ok(()) - } - pub fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<(usize, &mut Scene)> - { - let scene = Scene { - name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), - clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(ItemPalette::random), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok((index, &mut self.scenes_mut()[index])) - } - pub fn scene_default_name (&self) -> Arc { - format!("Sc{:3>}", self.scenes().len() + 1).into() - } -} -pub trait HasScenes: HasSelection + HasEditor + Send + Sync { - fn scenes (&self) -> &Vec; - fn scenes_mut (&mut self) -> &mut Vec; - fn scene_longest (&self) -> usize { - self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) - } - fn scene (&self) -> Option<&Scene> { - self.selected().scene().and_then(|s|self.scenes().get(s)) - } - fn scene_mut (&mut self) -> Option<&mut Scene> { - self.selected().scene().and_then(|s|self.scenes_mut().get_mut(s)) - } - fn scene_del (&mut self, index: usize) { - self.selected().scene().and_then(|s|Some(self.scenes_mut().remove(index))); - } -} -#[derive(Debug, Default)] pub struct Scene { - /// Name of scene - pub name: Arc, - /// Clips in scene, one per track - pub clips: Vec>>>, - /// Identifying color of scene - pub color: ItemPalette, -} -impl Scene { - /// Returns the pulse length of the longest clip in the scene - 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. - 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.player().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 } - } -} -impl HasScenes for Tek { - fn scenes (&self) -> &Vec { &self.scenes } - fn scenes_mut (&mut self) -> &mut Vec { &mut self.scenes } -} -#[cfg(test)] #[test] fn test_model_scene () { - let mut app = Tek::default(); - let _ = app.scene_longest(); - let _ = app.scene(); - let _ = app.scene_mut(); - let _ = app.scene_add(None, None); - app.scene_del(0); - - let scene = Scene::default(); - let _ = scene.pulses(); - let _ = scene.is_playing(&[]); -} diff --git a/crates/app/src/model/model_select.rs b/crates/app/src/model/model_select.rs deleted file mode 100644 index f35a0d42..00000000 --- a/crates/app/src/model/model_select.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::*; -pub trait HasSelection { - fn selected (&self) -> &Selection; - fn selected_mut (&mut self) -> &mut Selection; -} -/// Represents the current user selection in the arranger -#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { - /// The whole mix is selected - #[default] Mix, - /// A track is selected. - Track(usize), - /// A scene is selected. - Scene(usize), - /// A clip (track × scene) is selected. - Clip(usize, usize), -} -/// Focus identification methods -impl Selection { - fn is_mix (&self) -> bool { matches!(self, Self::Mix) } - fn is_track (&self) -> bool { matches!(self, Self::Track(_)) } - fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) } - fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) } - pub fn track (&self) -> Option { - use Selection::*; - match self { Clip(t, _) => Some(*t), Track(t) => Some(*t), _ => None } - } - pub fn scene (&self) -> Option { - use Selection::*; - match self { Clip(_, s) => Some(*s), Scene(s) => Some(*s), _ => None } - } - pub fn describe (&self, tracks: &[Track], scenes: &[Scene]) -> Arc { - format!("{}", match self { - Self::Mix => "Everything".to_string(), - Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)) - .unwrap_or_else(||"T??".into()), - Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)) - .unwrap_or_else(||"S??".into()), - Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { - (Some(_), Some(scene)) => match scene.clip(*t) { - Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), - None => format!("T{t} S{s}: Empty") - }, - _ => format!("T{t} S{s}: Empty"), - } - }).into() - } -} -impl HasSelection for Tek { - fn selected (&self) -> &Selection { &self.selected } - fn selected_mut (&mut self) -> &mut Selection { &mut self.selected } -} diff --git a/crates/app/src/model/model_track.rs b/crates/app/src/model/model_track.rs deleted file mode 100644 index 34c79331..00000000 --- a/crates/app/src/model/model_track.rs +++ /dev/null @@ -1,103 +0,0 @@ -use crate::*; -impl Tek { - pub fn tracks_add ( - &mut self, count: usize, width: Option, - midi_from: &[PortConnect], midi_to: &[PortConnect], - ) -> Usually<()> { - let jack = self.jack().clone(); - let track_color_1 = ItemColor::random(); - let track_color_2 = ItemColor::random(); - for i in 0..count { - let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); - let mut track = self.track_add(None, Some(color), midi_from, midi_to)?.1; - if let Some(width) = width { - track.width = width; - } - } - Ok(()) - } - pub fn track_add ( - &mut self, name: Option<&str>, color: Option, - midi_froms: &[PortConnect], - midi_tos: &[PortConnect], - ) -> Usually<(usize, &mut Track)> { - let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into()); - let mut track = Track { - width: (name.len() + 2).max(12), - color: color.unwrap_or_else(ItemPalette::random), - player: MidiPlayer::new( - &format!("{name}"), - self.jack(), - Some(self.clock()), - None, - midi_froms, - midi_tos - )?, - 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])) - } - pub fn track_del (&mut self, index: usize) { - self.tracks_mut().remove(index); - for scene in self.scenes_mut().iter_mut() { - scene.clips.remove(index); - } - } -} -pub trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync { - fn midi_ins (&self) -> &Vec; - fn midi_outs (&self) -> &Vec; - fn tracks (&self) -> &Vec; - fn tracks_mut (&mut self) -> &mut Vec; - fn track_longest (&self) -> usize { - self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) - } - const WIDTH_OFFSET: usize = 1; - fn track_next_name (&self) -> Arc { - format!("Track{:02}", self.tracks().len() + 1).into() - } - fn track (&self) -> Option<&Track> { - self.selected().track().and_then(|s|self.tracks().get(s)) - } - fn track_mut (&mut self) -> Option<&mut Track> { - self.selected().track().and_then(|s|self.tracks_mut().get_mut(s)) - } -} -#[derive(Debug, Default)] pub struct Track { - /// Name of track - pub name: Arc, - /// Preferred width of track column - pub width: usize, - /// Identifying color of track - pub color: ItemPalette, - /// MIDI player state - pub player: MidiPlayer, - /// Device chain - pub devices: Vec>, - /// Inputs of 1st device - pub audio_ins: Vec, - /// Outputs of last device - pub audio_outs: Vec, -} -has_clock!(|self: Track|self.player.clock); -has_player!(|self: Track|self.player); -impl 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 HasTracks for Tek { - fn midi_ins (&self) -> &Vec { &self.midi_ins } - fn midi_outs (&self) -> &Vec { &self.midi_outs } - fn tracks (&self) -> &Vec { &self.tracks } - fn tracks_mut (&mut self) -> &mut Vec { &mut self.tracks } -} diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index 8ac4233c..4bf06558 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -1,13 +1,7 @@ use crate::*; -mod view_clock; pub use self::view_clock::*; -mod view_memo; pub use self::view_memo::*; -mod view_meter; pub use self::view_meter::*; -mod view_ports; pub use self::view_ports::*; -mod view_layout; pub use self::view_layout::*; pub(crate) use std::fmt::Write; pub(crate) use ::tengri::tui::ratatui::prelude::Position; -pub(crate) trait ScenesColors<'a> = Iterator>; -pub(crate) type SceneWithColor<'a> = (usize, &'a Scene, usize, usize, Option); + pub(crate) struct ArrangerView<'a> { app: &'a Tek, @@ -88,6 +82,143 @@ impl<'a> ArrangerView<'a> { } } + /// Render input matrix. + pub(crate) fn inputs (&'a self) -> impl Content + 'a { + Tui::bg(Color::Reset, + Bsp::s(Bsp::s(self.input_routes(), self.input_ports()), self.input_intos())) + } + + fn input_routes (&'a self) -> impl Content + 'a { + Tryptich::top(self.inputs_height) + .left(self.width_side, + io_ports(Tui::g(224), Tui::g(32), ||self.app.inputs_with_sizes())) + .middle(self.width_mid, + per_track_top( + self.width_mid, + ||self.app.tracks_with_sizes(), + move|_, &Track { color, .. }|{ + io_conns(color.dark.rgb, color.darker.rgb, ||self.app.inputs_with_sizes()) + } + )) + } + + fn input_ports (&'a self) -> impl Content + 'a { + Tryptich::top(1) + .left(self.width_side, + button_3("i", "midi ins", format!("{}", self.inputs_count), self.is_editing)) + .right(self.width_side, + button_2("I", "add midi in", self.is_editing)) + .middle(self.width_mid, + per_track_top( + self.width_mid, + ||self.app.tracks_with_sizes(), + move|t, track|{ + let rec = track.player.recording; + let mon = track.player.monitoring; + let rec = if rec { White } else { track.color.darkest.rgb }; + let mon = if mon { White } else { track.color.darkest.rgb }; + let bg = if self.track_selected == Some(t) { + track.color.light.rgb + } else { + track.color.base.rgb + }; + //let bg2 = if t > 0 { track.color.base.rgb } else { Reset }; + wrap(bg, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e( + Tui::fg_bg(rec, bg, "Rec "), + Tui::fg_bg(mon, bg, "Mon "), + )))) + })) + } + + fn input_intos (&'a self) -> impl Content + 'a { + Tryptich::top(2) + .left(self.width_side, + Bsp::s(Align::e("Input:"), Align::e("Into:"))) + .middle(self.width_mid, + per_track_top( + self.width_mid, + ||self.app.tracks_with_sizes(), + |_, _|{ + Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ "))) + })) + } + + /// Render output matrix. + pub(crate) fn outputs (&'a self) -> impl Content + 'a { + Tui::bg(Color::Reset, Align::n(Bsp::s( + Bsp::s( + self.output_nexts(), + self.output_froms(), + ), + Bsp::s( + self.output_ports(), + self.output_conns(), + ) + ))) + } + + fn output_nexts (&'a self) -> impl Content + 'a { + Tryptich::top(2) + .left(self.width_side, Align::ne("From:")) + .middle(self.width_mid, per_track_top( + self.width_mid, + ||self.tracks_with_sizes_scrolled(), + |_, _|{ + Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default()))) + })) + } + fn output_froms (&'a self) -> impl Content + 'a { + Tryptich::top(2) + .left(self.width_side, Align::ne("Next:")) + .middle(self.width_mid, per_track_top( + self.width_mid, + ||self.tracks_with_sizes_scrolled(), + |t, track|Either( + track.player.next_clip.is_some(), + Thunk::new(||Tui::bg(Reset, format!("{:?}", + track.player.next_clip.as_ref() + .map(|(moment, clip)|clip.as_ref() + .map(|clip|clip.read().unwrap().name.clone())) + .flatten().as_ref()))), + Thunk::new(||Tui::bg(Reset, " ------ ")) + ))) + } + fn output_ports (&'a self) -> impl Content + 'a { + Tryptich::top(1) + .left(self.width_side, + button_3("o", "midi outs", format!("{}", self.outputs_count), self.is_editing)) + .right(self.width_side, + button_2("O", "add midi out", self.is_editing)) + .middle(self.width_mid, + per_track_top( + self.width_mid, + ||self.tracks_with_sizes_scrolled(), + move|i, t|{ + let mute = false; + let solo = false; + let mute = if mute { White } else { t.color.darkest.rgb }; + let solo = if solo { White } else { t.color.darkest.rgb }; + let bg_1 = if self.track_selected == Some(i) { t.color.light.rgb } else { t.color.base.rgb }; + let bg_2 = if i > 0 { t.color.base.rgb } else { Reset }; + let mute = Tui::fg_bg(mute, bg_1, "Play "); + let solo = Tui::fg_bg(solo, bg_1, "Solo "); + wrap(bg_1, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(mute, solo)))) + })) + } + fn output_conns (&'a self) -> impl Content + 'a { + Tryptich::top(self.outputs_height) + .left(self.width_side, + io_ports(Tui::g(224), Tui::g(32), ||self.app.outputs_with_sizes())) + .middle(self.width_mid, per_track_top( + self.width_mid, + ||self.tracks_with_sizes_scrolled(), + |_, t|io_conns( + t.color.dark.rgb, + t.color.darker.rgb, + ||self.app.outputs_with_sizes() + ))) + } + /// Render track headers pub(crate) fn tracks (&'a self) -> impl Content + 'a { let Self { width_side, width_mid, track_count, track_selected, is_editing, .. } = self; @@ -238,6 +369,10 @@ impl<'a> ArrangerView<'a> { } +trait ScenesColors<'a> = Iterator>; + +type SceneWithColor<'a> = (usize, &'a Scene, usize, usize, Option); + impl Tek { /// Spacing between tracks. pub(crate) const TRACK_SPACING: usize = 0; @@ -345,36 +480,356 @@ impl Tek { }) } + fn update_clock (&self) { + ViewCache::update_clock(&self.view_cache, self.clock(), self.size.w() > 80) + } + + pub fn view_transport (&self) -> impl Content + use<'_> { + self.update_clock(); + let cache = self.view_cache.read().unwrap(); + view_transport( + self.clock.is_rolling(), + cache.bpm.view.clone(), + cache.beat.view.clone(), + cache.time.view.clone(), + ) + } + + pub fn view_status (&self) -> impl Content + use<'_> { + self.update_clock(); + let cache = self.view_cache.read().unwrap(); + view_status( + self.selected.describe(&self.tracks, &self.scenes), + cache.sr.view.clone(), + cache.buf.view.clone(), + cache.lat.view.clone(), + ) + } + } + /// Define a type alias for iterators of sized items (columns). macro_rules! def_sizes_iter { ($Type:ident => $($Item:ty),+) => { pub(crate) trait $Type<'a> = - Iterator + Send + Sync + 'a;}} + Iterator + Send + Sync + 'a; + } +} + def_sizes_iter!(ScenesSizes => Scene); + def_sizes_iter!(TracksSizes => Track); + def_sizes_iter!(InputsSizes => JackMidiIn); + def_sizes_iter!(OutputsSizes => JackMidiOut); + def_sizes_iter!(PortsSizes => Arc, [PortConnect]); -#[cfg(test)] #[test] fn test_view_iter () { - let mut tek = Tek::default(); - tek.editor = Some(Default::default()); - let _: Vec<_> = tek.inputs_with_sizes().collect(); - let _: Vec<_> = tek.outputs_with_sizes().collect(); - let _: Vec<_> = tek.tracks_with_sizes().collect(); - let _: Vec<_> = tek.scenes_with_sizes(true, 10, 10).collect(); - //let _: Vec<_> = tek.scenes_with_colors(true, 10).collect(); - //let _: Vec<_> = tek.scenes_with_track_colors(true, 10, 10).collect(); +fn view_transport ( + play: bool, + bpm: Arc>, + beat: Arc>, + time: Arc>, +) -> impl Content { + let theme = ItemPalette::G[96]; + Tui::bg(Black, row!(Bsp::a( + Fill::xy(Align::w(button_play_pause(play))), + Fill::xy(Align::e(row!( + FieldH(theme, "BPM", bpm), + FieldH(theme, "Beat", beat), + FieldH(theme, "Time", time), + ))) + ))) } -#[cfg(test)] #[test] fn test_view_sizes () { - let app = Tek::default(); - let _ = app.w(); - let _ = app.w_sidebar(); - let _ = app.w_tracks_area(); - let _ = app.h(); - let _ = app.h_tracks_area(); - let _ = app.h_inputs(); - let _ = app.h_outputs(); - let _ = app.h_scenes(); + +fn view_status ( + sel: Arc, + sr: Arc>, + buf: Arc>, + lat: Arc>, +) -> impl Content { + let theme = ItemPalette::G[96]; + Tui::bg(Black, row!(Bsp::a( + Fill::xy(Align::w(FieldH(theme, "Selected", sel))), + Fill::xy(Align::e(row!( + FieldH(theme, "SR", sr), + FieldH(theme, "Buf", buf), + FieldH(theme, "Lat", lat), + ))) + ))) +} + +fn button_play_pause (playing: bool) -> impl Content { + let compact = true;//self.is_editing(); + Tui::bg( + if playing{Rgb(0,128,0)}else{Rgb(128,64,0)}, + Either::new(compact, + Thunk::new(move||Fixed::x(9, Either::new(playing, + Tui::fg(Rgb(0, 255, 0), " PLAYING "), + Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + ), + Thunk::new(move||Fixed::x(5, Either::new(playing, + Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))) + ) + ) + ) +} + +fn view_meter <'a> (label: &'a str, value: f32) -> impl Content + 'a { + col!( + FieldH(ItemPalette::G[128], label, format!("{:>+9.3}", value)), + Fixed::xy(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 }, ()))) +} + +fn view_meters (values: &[f32;2]) -> impl Content + use<'_> { + Bsp::s( + format!("L/{:>+9.3}", values[0]), + format!("R/{:>+9.3}", values[1]), + ) +} + +pub(crate) fn wrap ( + bg: Color, fg: Color, content: impl Content +) -> impl Content { + Bsp::e(Tui::fg_bg(bg, Reset, "▐"), + Bsp::w(Tui::fg_bg(bg, Reset, "▌"), + Tui::fg_bg(fg, bg, content))) +} + +pub(crate) fn button_2 <'a, K, L> ( + key: K, label: L, editing: bool, +) -> impl Content + 'a where + K: Content + 'a, + L: Content + 'a, +{ + let key = Tui::fg_bg(Tui::g(0), Tui::orange(), Bsp::e( + Tui::fg_bg(Tui::orange(), Reset, "▐"), + Bsp::e(key, Tui::fg(Tui::g(96), "▐")) + )); + let label = When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label)); + Tui::bold(true, Bsp::e(key, label)) +} + +pub(crate) fn button_3 <'a, K, L, V> ( + key: K, + label: L, + value: V, + editing: bool, +) -> impl Content + 'a where + K: Content + 'a, + L: Content + 'a, + V: Content + 'a, +{ + let key = Tui::fg_bg(Tui::g(0), Tui::orange(), + Bsp::e(Tui::fg_bg(Tui::orange(), Reset, "▐"), Bsp::e(key, Tui::fg(if editing { + Tui::g(128) + } else { + Tui::g(96) + }, "▐")))); + let label = Bsp::e( + When::new(!editing, Bsp::e( + Tui::fg_bg(Tui::g(255), Tui::g(96), label), + Tui::fg_bg(Tui::g(128), Tui::g(96), "▐"), + )), + Bsp::e( + Tui::fg_bg(Tui::g(224), Tui::g(128), value), + Tui::fg_bg(Tui::g(128), Reset, "▌"), + )); + Tui::bold(true, Bsp::e(key, label)) +} + +pub(crate) fn heading <'a> ( + key: &'a str, + label: &'a str, + count: usize, + content: impl Content + Send + Sync + 'a, + editing: bool, +) -> impl Content + 'a { + let count = format!("{count}"); + Fill::xy(Align::w(Bsp::s(Fill::x(Align::w(button_3(key, label, count, editing))), content))) +} + +pub(crate) fn io_ports <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a +) -> impl Content + 'a { + Map::new(iter, + move|(index, name, connections, y, y2): (usize, &'a Arc, &'a [PortConnect], usize, usize), _| + map_south(y as u16, (y2-y) as u16, Bsp::s( + Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(" 󰣲 ", name))))), + Map::new(||connections.iter(), move|connect: &'a PortConnect, index|map_south(index as u16, 1, + Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, + &connect.info))))))))) +} + +pub(crate) fn io_conns <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a +) -> impl Content + 'a { + Map::new(iter, + move|(index, name, connections, y, y2): (usize, &'a Arc, &'a [PortConnect], usize, usize), _| + map_south(y as u16, (y2-y) as u16, Bsp::s( + Fill::x(Tui::bold(true, wrap(bg, fg, Fill::x(Align::w("▞▞▞▞ ▞▞▞▞"))))), + Map::new(||connections.iter(), move|connect, index|map_south(index as u16, 1, + Fill::x(Align::w(Tui::bold(false, wrap(bg, fg, Fill::x("")))))))))) +} + +pub(crate) fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( + width: u16, + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Content + 'a { + Align::x(Tui::bg(Reset, Map::new(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + let width = (x2 - x1) as u16; + map_east(x1 as u16, width, Fixed::x(width, Tui::fg_bg( + track.color.lightest.rgb, + track.color.base.rgb, + callback(index, track))))}))) +} + +pub(crate) fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( + width: u16, + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Content + 'a { + per_track_top( + width, + tracks, + move|index, track|Fill::y(Align::y(callback(index, track))) + ) +} + +/// Clear a pre-allocated buffer, then write into it. +#[macro_export] macro_rules! rewrite { + ($buf:ident, $($rest:tt)*) => { |$buf,_,_|{ $buf.clear(); write!($buf, $($rest)*) } } +} + +#[derive(Debug, Default)] pub(crate) struct ViewMemo { + pub(crate) value: T, + pub(crate) view: Arc> +} + +impl ViewMemo { + fn new (value: T, view: U) -> Self { + Self { value, view: Arc::new(view.into()) } + } + pub(crate) fn update ( + &mut self, + newval: T, + render: impl Fn(&mut U, &T, &T)->R + ) -> Option { + if newval != self.value { + let result = render(&mut*self.view.write().unwrap(), &newval, &self.value); + self.value = newval; + return Some(result); + } + None + } +} + +#[derive(Debug)] pub struct ViewCache { + pub(crate) sr: ViewMemo, String>, + pub(crate) buf: ViewMemo, String>, + pub(crate) lat: ViewMemo, String>, + pub(crate) bpm: ViewMemo, String>, + pub(crate) beat: ViewMemo, String>, + pub(crate) time: ViewMemo, String>, + pub(crate) scns: ViewMemo, String>, + pub(crate) trks: ViewMemo, String>, + pub(crate) stop: Arc, + pub(crate) edit: Arc, +} + +impl Default for ViewCache { + fn default () -> Self { + let mut beat = String::with_capacity(16); + write!(beat, "{}", Self::BEAT_EMPTY); + let mut time = String::with_capacity(16); + write!(time, "{}", Self::TIME_EMPTY); + let mut bpm = String::with_capacity(16); + write!(bpm, "{}", Self::BPM_EMPTY); + Self { + beat: ViewMemo::new(None, beat), + time: ViewMemo::new(None, time), + bpm: ViewMemo::new(None, bpm), + sr: ViewMemo::new(None, String::with_capacity(16)), + buf: ViewMemo::new(None, String::with_capacity(16)), + lat: ViewMemo::new(None, String::with_capacity(16)), + scns: ViewMemo::new(None, String::with_capacity(16)), + trks: ViewMemo::new(None, String::with_capacity(16)), + stop: "⏹".into(), + edit: "edit".into(), + } + } +} + +impl ViewCache { + pub const BEAT_EMPTY: &'static str = "-.-.--"; + pub const TIME_EMPTY: &'static str = "-.---s"; + pub const BPM_EMPTY: &'static str = "---.---"; + + pub(crate) fn track_counter (cache: &Arc>, track: usize, tracks: usize) + -> Arc> + { + let data = (track, tracks); + cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1)); + cache.read().unwrap().trks.view.clone() + } + + pub(crate) fn scene_add (cache: &Arc>, scene: usize, scenes: usize, is_editing: bool) + -> impl Content + { + let data = (scene, scenes); + cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1)); + button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing) + } + + pub(crate) 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, "{}", ViewCache::BEAT_EMPTY)); + cache.time.update(None, rewrite!(buf, "{}", ViewCache::TIME_EMPTY)); + cache.bpm.update(None, rewrite!(buf, "{}", ViewCache::BPM_EMPTY)); + } + } } diff --git a/crates/app/src/view/view_clock.rs b/crates/app/src/view/view_clock.rs deleted file mode 100644 index b785b090..00000000 --- a/crates/app/src/view/view_clock.rs +++ /dev/null @@ -1,88 +0,0 @@ -use crate::*; -impl Tek { - fn update_clock (&self) { - ViewCache::update_clock(&self.view_cache, self.clock(), self.size.w() > 80) - } - pub(crate) fn view_transport (&self) -> impl Content + use<'_> { - self.update_clock(); - let cache = self.view_cache.read().unwrap(); - view_transport( - self.clock.is_rolling(), - cache.bpm.view.clone(), - cache.beat.view.clone(), - cache.time.view.clone(), - ) - } - pub(crate) fn view_status (&self) -> impl Content + use<'_> { - self.update_clock(); - let cache = self.view_cache.read().unwrap(); - view_status( - self.selected.describe(&self.tracks, &self.scenes), - cache.sr.view.clone(), - cache.buf.view.clone(), - cache.lat.view.clone(), - ) - } -} - -fn view_transport ( - play: bool, - bpm: Arc>, - beat: Arc>, - time: Arc>, -) -> impl Content { - let theme = ItemPalette::G[96]; - Tui::bg(Black, row!(Bsp::a( - Fill::xy(Align::w(button_play_pause(play))), - Fill::xy(Align::e(row!( - FieldH(theme, "BPM", bpm), - FieldH(theme, "Beat", beat), - FieldH(theme, "Time", time), - ))) - ))) -} - -fn view_status ( - sel: Arc, - sr: Arc>, - buf: Arc>, - lat: Arc>, -) -> impl Content { - let theme = ItemPalette::G[96]; - Tui::bg(Black, row!(Bsp::a( - Fill::xy(Align::w(FieldH(theme, "Selected", sel))), - Fill::xy(Align::e(row!( - FieldH(theme, "SR", sr), - FieldH(theme, "Buf", buf), - FieldH(theme, "Lat", lat), - ))) - ))) -} - -fn button_play_pause (playing: bool) -> impl Content { - let compact = true;//self.is_editing(); - Tui::bg( - if playing{Rgb(0,128,0)}else{Rgb(128,64,0)}, - Either::new(compact, - Thunk::new(move||Fixed::x(9, Either::new(playing, - Tui::fg(Rgb(0, 255, 0), " PLAYING "), - Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - ), - Thunk::new(move||Fixed::x(5, Either::new(playing, - Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))) - ) - ) - ) -} - -#[cfg(test)] mod test { - use super::*; - #[test] fn test_view_clock () { - let _ = button_play_pause(true); - let mut app = Tek::default(); - let _ = app.view_transport(); - let _ = app.view_status(); - let _ = app.update_clock(); - } -} diff --git a/crates/app/src/view/view_layout.rs b/crates/app/src/view/view_layout.rs deleted file mode 100644 index 34fb37e8..00000000 --- a/crates/app/src/view/view_layout.rs +++ /dev/null @@ -1,178 +0,0 @@ -use crate::*; - -/// A three-column layout. -pub(crate) struct Tryptich { - pub top: bool, - pub h: u16, - pub left: (u16, A), - pub middle: (u16, B), - pub right: (u16, C), -} - -impl Tryptich<(), (), ()> { - pub fn center (h: u16) -> Self { - Self { h, top: false, left: (0, ()), middle: (0, ()), right: (0, ()) } - } - pub fn top (h: u16) -> Self { - Self { h, top: true, left: (0, ()), middle: (0, ()), right: (0, ()) } - } -} - -impl Tryptich { - pub fn left (self, w: u16, content: D) -> Tryptich { - Tryptich { left: (w, content), ..self } - } - pub fn middle (self, w: u16, content: D) -> Tryptich { - Tryptich { middle: (w, content), ..self } - } - pub fn right (self, w: u16, content: D) -> Tryptich { - Tryptich { right: (w, content), ..self } - } -} - -impl Content for Tryptich -where A: Content, B: Content, C: Content { - fn content (&self) -> impl Render { - let Self { top, h, left: (w_a, ref a), middle: (w_b, ref b), right: (w_c, ref c) } = *self; - Fixed::y(h, if top { - Bsp::a( - Fill::x(Align::n(Fixed::x(w_b, Align::x(Tui::bg(Reset, b))))), - Bsp::a( - Fill::x(Align::nw(Fixed::x(w_a, Tui::bg(Reset, a)))), - Fill::x(Align::ne(Fixed::x(w_c, Tui::bg(Reset, c)))), - ), - ) - } else { - Bsp::a( - Fill::xy(Align::c(Fixed::x(w_b, Align::x(Tui::bg(Reset, b))))), - Bsp::a( - Fill::xy(Align::w(Fixed::x(w_a, Tui::bg(Reset, a)))), - Fill::xy(Align::e(Fixed::x(w_c, Tui::bg(Reset, c)))), - ), - ) - }) - } -} - -pub(crate) fn wrap ( - bg: Color, fg: Color, content: impl Content -) -> impl Content { - Bsp::e(Tui::fg_bg(bg, Reset, "▐"), - Bsp::w(Tui::fg_bg(bg, Reset, "▌"), - Tui::fg_bg(fg, bg, content))) -} - -pub(crate) fn button_2 <'a, K, L> ( - key: K, label: L, editing: bool, -) -> impl Content + 'a where - K: Content + 'a, - L: Content + 'a, -{ - let key = Tui::fg_bg(Tui::g(0), Tui::orange(), Bsp::e( - Tui::fg_bg(Tui::orange(), Reset, "▐"), - Bsp::e(key, Tui::fg(Tui::g(96), "▐")) - )); - let label = When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label)); - Tui::bold(true, Bsp::e(key, label)) -} - -pub(crate) fn button_3 <'a, K, L, V> ( - key: K, - label: L, - value: V, - editing: bool, -) -> impl Content + 'a where - K: Content + 'a, - L: Content + 'a, - V: Content + 'a, -{ - let key = Tui::fg_bg(Tui::g(0), Tui::orange(), - Bsp::e(Tui::fg_bg(Tui::orange(), Reset, "▐"), Bsp::e(key, Tui::fg(if editing { - Tui::g(128) - } else { - Tui::g(96) - }, "▐")))); - let label = Bsp::e( - When::new(!editing, Bsp::e( - Tui::fg_bg(Tui::g(255), Tui::g(96), label), - Tui::fg_bg(Tui::g(128), Tui::g(96), "▐"), - )), - Bsp::e( - Tui::fg_bg(Tui::g(224), Tui::g(128), value), - Tui::fg_bg(Tui::g(128), Reset, "▌"), - )); - Tui::bold(true, Bsp::e(key, label)) -} - -pub(crate) fn heading <'a> ( - key: &'a str, - label: &'a str, - count: usize, - content: impl Content + Send + Sync + 'a, - editing: bool, -) -> impl Content + 'a { - let count = format!("{count}"); - Fill::xy(Align::w(Bsp::s(Fill::x(Align::w(button_3(key, label, count, editing))), content))) -} - -pub(crate) fn io_ports <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a -) -> impl Content + 'a { - Map::new(iter, - move|(index, name, connections, y, y2): (usize, &'a Arc, &'a [PortConnect], usize, usize), _| - map_south(y as u16, (y2-y) as u16, Bsp::s( - Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(" 󰣲 ", name))))), - Map::new(||connections.iter(), move|connect: &'a PortConnect, index|map_south(index as u16, 1, - Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, - &connect.info))))))))) -} - -pub(crate) fn io_conns <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a -) -> impl Content + 'a { - Map::new(iter, - move|(index, name, connections, y, y2): (usize, &'a Arc, &'a [PortConnect], usize, usize), _| - map_south(y as u16, (y2-y) as u16, Bsp::s( - Fill::x(Tui::bold(true, wrap(bg, fg, Fill::x(Align::w("▞▞▞▞ ▞▞▞▞"))))), - Map::new(||connections.iter(), move|connect, index|map_south(index as u16, 1, - Fill::x(Align::w(Tui::bold(false, wrap(bg, fg, Fill::x("")))))))))) -} - -pub(crate) fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( - width: u16, - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Content + 'a { - Align::x(Tui::bg(Reset, Map::new(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - map_east(x1 as u16, width, Fixed::x(width, Tui::fg_bg( - track.color.lightest.rgb, - track.color.base.rgb, - callback(index, track))))}))) -} - -pub(crate) fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( - width: u16, - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Content + 'a { - per_track_top( - width, - tracks, - move|index, track|Fill::y(Align::y(callback(index, track))) - ) -} - -#[cfg(test)] mod test { - use super::*; - #[test] fn test_view () { - let _ = button_2("", "", true); - let _ = button_2("", "", false); - let _ = button_3("", "", "", true); - let _ = button_3("", "", "", false); - let _ = heading("", "", 0, "", true); - let _ = heading("", "", 0, "", false); - let _ = wrap(Reset, Reset, ""); - } -} diff --git a/crates/app/src/view/view_memo.rs b/crates/app/src/view/view_memo.rs deleted file mode 100644 index 03c3eb23..00000000 --- a/crates/app/src/view/view_memo.rs +++ /dev/null @@ -1,120 +0,0 @@ -use crate::*; - -/// Clear a pre-allocated buffer, then write into it. -#[macro_export] macro_rules! rewrite { - ($buf:ident, $($rest:tt)*) => { |$buf,_,_|{ $buf.clear(); write!($buf, $($rest)*) } } -} - -#[derive(Debug, Default)] pub(crate) struct ViewMemo { - pub(crate) value: T, - pub(crate) view: Arc> -} - -impl ViewMemo { - fn new (value: T, view: U) -> Self { - Self { value, view: Arc::new(view.into()) } - } - pub(crate) fn update ( - &mut self, - newval: T, - render: impl Fn(&mut U, &T, &T)->R - ) -> Option { - if newval != self.value { - let result = render(&mut*self.view.write().unwrap(), &newval, &self.value); - self.value = newval; - return Some(result); - } - None - } -} - -#[derive(Debug)] pub struct ViewCache { - pub(crate) sr: ViewMemo, String>, - pub(crate) buf: ViewMemo, String>, - pub(crate) lat: ViewMemo, String>, - pub(crate) bpm: ViewMemo, String>, - pub(crate) beat: ViewMemo, String>, - pub(crate) time: ViewMemo, String>, - pub(crate) scns: ViewMemo, String>, - pub(crate) trks: ViewMemo, String>, - pub(crate) stop: Arc, - pub(crate) edit: Arc, -} - -impl Default for ViewCache { - fn default () -> Self { - let mut beat = String::with_capacity(16); - write!(beat, "{}", Self::BEAT_EMPTY); - let mut time = String::with_capacity(16); - write!(time, "{}", Self::TIME_EMPTY); - let mut bpm = String::with_capacity(16); - write!(bpm, "{}", Self::BPM_EMPTY); - Self { - beat: ViewMemo::new(None, beat), - time: ViewMemo::new(None, time), - bpm: ViewMemo::new(None, bpm), - sr: ViewMemo::new(None, String::with_capacity(16)), - buf: ViewMemo::new(None, String::with_capacity(16)), - lat: ViewMemo::new(None, String::with_capacity(16)), - scns: ViewMemo::new(None, String::with_capacity(16)), - trks: ViewMemo::new(None, String::with_capacity(16)), - stop: "⏹".into(), - edit: "edit".into(), - } - } -} - -impl ViewCache { - pub const BEAT_EMPTY: &'static str = "-.-.--"; - pub const TIME_EMPTY: &'static str = "-.---s"; - pub const BPM_EMPTY: &'static str = "---.---"; - - pub(crate) fn track_counter (cache: &Arc>, track: usize, tracks: usize) - -> Arc> - { - let data = (track, tracks); - cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1)); - cache.read().unwrap().trks.view.clone() - } - - pub(crate) fn scene_add (cache: &Arc>, scene: usize, scenes: usize, is_editing: bool) - -> impl Content - { - let data = (scene, scenes); - cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1)); - button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing) - } - - pub(crate) 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, "{}", ViewCache::BEAT_EMPTY)); - cache.time.update(None, rewrite!(buf, "{}", ViewCache::TIME_EMPTY)); - cache.bpm.update(None, rewrite!(buf, "{}", ViewCache::BPM_EMPTY)); - } - } -} diff --git a/crates/app/src/view/view_meter.rs b/crates/app/src/view/view_meter.rs deleted file mode 100644 index 378de0a9..00000000 --- a/crates/app/src/view/view_meter.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::*; -fn view_meter <'a> (label: &'a str, value: f32) -> impl Content + 'a { - col!( - FieldH(ItemPalette::G[128], label, format!("{:>+9.3}", value)), - Fixed::xy(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 }, ()))) -} -fn view_meters (values: &[f32;2]) -> impl Content + use<'_> { - Bsp::s( - format!("L/{:>+9.3}", values[0]), - format!("R/{:>+9.3}", values[1]), - ) -} -#[cfg(test)] mod test_view_meter { - use super::*; - use proptest::prelude::*; - #[test] fn test_view_meter () { - let _ = view_meter("", 0.0); - let _ = view_meters(&[0.0, 0.0]); - } - 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]); - } - } -} diff --git a/crates/app/src/view/view_ports.rs b/crates/app/src/view/view_ports.rs deleted file mode 100644 index 88c5b11f..00000000 --- a/crates/app/src/view/view_ports.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::*; - -impl<'a> ArrangerView<'a> { - /// Render input matrix. - pub(crate) fn inputs (&'a self) -> impl Content + 'a { - Tui::bg(Color::Reset, - Bsp::s(Bsp::s(self.input_routes(), self.input_ports()), self.input_intos())) - } - - fn input_routes (&'a self) -> impl Content + 'a { - Tryptich::top(self.inputs_height) - .left(self.width_side, - io_ports(Tui::g(224), Tui::g(32), ||self.app.inputs_with_sizes())) - .middle(self.width_mid, - per_track_top( - self.width_mid, - ||self.app.tracks_with_sizes(), - move|_, &Track { color, .. }|{ - io_conns(color.dark.rgb, color.darker.rgb, ||self.app.inputs_with_sizes()) - } - )) - } - - fn input_ports (&'a self) -> impl Content + 'a { - Tryptich::top(1) - .left(self.width_side, - button_3("i", "midi ins", format!("{}", self.inputs_count), self.is_editing)) - .right(self.width_side, - button_2("I", "add midi in", self.is_editing)) - .middle(self.width_mid, - per_track_top( - self.width_mid, - ||self.app.tracks_with_sizes(), - move|t, track|{ - let rec = track.player.recording; - let mon = track.player.monitoring; - let rec = if rec { White } else { track.color.darkest.rgb }; - let mon = if mon { White } else { track.color.darkest.rgb }; - let bg = if self.track_selected == Some(t) { - track.color.light.rgb - } else { - track.color.base.rgb - }; - //let bg2 = if t > 0 { track.color.base.rgb } else { Reset }; - wrap(bg, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e( - Tui::fg_bg(rec, bg, "Rec "), - Tui::fg_bg(mon, bg, "Mon "), - )))) - })) - } - - fn input_intos (&'a self) -> impl Content + 'a { - Tryptich::top(2) - .left(self.width_side, - Bsp::s(Align::e("Input:"), Align::e("Into:"))) - .middle(self.width_mid, - per_track_top( - self.width_mid, - ||self.app.tracks_with_sizes(), - |_, _|{ - Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ "))) - })) - } - - /// Render output matrix. - pub(crate) fn outputs (&'a self) -> impl Content + 'a { - Tui::bg(Color::Reset, Align::n(Bsp::s( - Bsp::s( - self.output_nexts(), - self.output_froms(), - ), - Bsp::s( - self.output_ports(), - self.output_conns(), - ) - ))) - } - - fn output_nexts (&'a self) -> impl Content + 'a { - Tryptich::top(2) - .left(self.width_side, Align::ne("From:")) - .middle(self.width_mid, per_track_top( - self.width_mid, - ||self.tracks_with_sizes_scrolled(), - |_, _|{ - Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default()))) - })) - } - fn output_froms (&'a self) -> impl Content + 'a { - Tryptich::top(2) - .left(self.width_side, Align::ne("Next:")) - .middle(self.width_mid, per_track_top( - self.width_mid, - ||self.tracks_with_sizes_scrolled(), - |t, track|Either( - track.player.next_clip.is_some(), - Thunk::new(||Tui::bg(Reset, format!("{:?}", - track.player.next_clip.as_ref() - .map(|(moment, clip)|clip.as_ref() - .map(|clip|clip.read().unwrap().name.clone())) - .flatten().as_ref()))), - Thunk::new(||Tui::bg(Reset, " ------ ")) - ))) - } - fn output_ports (&'a self) -> impl Content + 'a { - Tryptich::top(1) - .left(self.width_side, - button_3("o", "midi outs", format!("{}", self.outputs_count), self.is_editing)) - .right(self.width_side, - button_2("O", "add midi out", self.is_editing)) - .middle(self.width_mid, - per_track_top( - self.width_mid, - ||self.tracks_with_sizes_scrolled(), - move|i, t|{ - let mute = false; - let solo = false; - let mute = if mute { White } else { t.color.darkest.rgb }; - let solo = if solo { White } else { t.color.darkest.rgb }; - let bg_1 = if self.track_selected == Some(i) { t.color.light.rgb } else { t.color.base.rgb }; - let bg_2 = if i > 0 { t.color.base.rgb } else { Reset }; - let mute = Tui::fg_bg(mute, bg_1, "Play "); - let solo = Tui::fg_bg(solo, bg_1, "Solo "); - wrap(bg_1, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(mute, solo)))) - })) - } - fn output_conns (&'a self) -> impl Content + 'a { - Tryptich::top(self.outputs_height) - .left(self.width_side, - io_ports(Tui::g(224), Tui::g(32), ||self.app.outputs_with_sizes())) - .middle(self.width_mid, per_track_top( - self.width_mid, - ||self.tracks_with_sizes_scrolled(), - |_, t|io_conns( - t.color.dark.rgb, - t.color.darker.rgb, - ||self.app.outputs_with_sizes() - ))) - } -} diff --git a/crates/cli/edn/sampler.edn b/crates/cli/edn/sampler.edn new file mode 100644 index 00000000..53f62325 --- /dev/null +++ b/crates/cli/edn/sampler.edn @@ -0,0 +1,3 @@ +(bsp/s (fixed/y 1 :transport) + (bsp/n (fixed/y 1 :status) + (fill/xy :samples-grid))) diff --git a/crates/cli/tek.rs b/crates/cli/tek.rs index 1e7c13d6..d87c44de 100644 --- a/crates/cli/tek.rs +++ b/crates/cli/tek.rs @@ -100,6 +100,7 @@ impl Cli { Mode::Sequencer => include_str!("./edn/sequencer.edn"), Mode::Groovebox => include_str!("./edn/groovebox.edn"), Mode::Arranger { .. } => include_str!("./edn/arranger.edn"), + Mode::Sampler => include_str!("./edn/sampler.edn"), _ => todo!("{mode:?}"), }), pool: match mode { @@ -115,7 +116,7 @@ impl Cli { midi_ins, midi_outs, midi_buf: match mode { - Mode::Clock => vec![], + Mode::Clock | Mode::Sampler => vec![], Mode::Sequencer | Mode::Groovebox | Mode::Arranger {..} => vec![vec![];65536], _ => todo!("{mode:?}"), }, @@ -130,13 +131,13 @@ impl Cli { Mode::Sequencer => vec![Track::default()], Mode::Groovebox => vec![Track { devices: vec![ - Sampler::new( + Device::boxed(Sampler::new( jack, &"sampler", midi_froms.as_slice(), audio_froms, audio_tos - )?.boxed() + )?) ], ..Track::default() }], diff --git a/crates/sampler/src/keys_sampler.edn b/crates/sampler/edn/sampler_keys.edn similarity index 100% rename from crates/sampler/src/keys_sampler.edn rename to crates/sampler/edn/sampler_keys.edn diff --git a/crates/sampler/edn/sampler_view.edn b/crates/sampler/edn/sampler_view.edn new file mode 100644 index 00000000..e69de29b diff --git a/crates/sampler/sampler_scratch.rs b/crates/sampler/sampler_scratch.rs new file mode 100644 index 00000000..a36805f6 --- /dev/null +++ b/crates/sampler/sampler_scratch.rs @@ -0,0 +1,93 @@ + +//impl Handle for AddSampleModal { + //fn handle (&mut self, from: &TuiIn) -> Perhaps { + //if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? { + //return Ok(Some(true)) + //} + //Ok(Some(true)) + //} +//} +//pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { + //[Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ + //modal.exit(); + //Ok(true) + //}], + //[Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{ + //modal.prev(); + //Ok(true) + //}], + //[Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{ + //modal.next(); + //Ok(true) + //}], + //[Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{ + //if modal.pick()? { + //modal.exit(); + //} + //Ok(true) + //}], + //[Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{ + //modal.try_preview()?; + //Ok(true) + //}] +//}); +//from_atom!("sampler" => |jack: &Jack, args| -> crate::Sampler { + //let mut name = String::new(); + //let mut dir = String::new(); + //let mut samples = BTreeMap::new(); + //atom!(atom in args { + //Atom::Map(map) => { + //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { + //name = String::from(*n); + //} + //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":dir")) { + //dir = String::from(*n); + //} + //}, + //Atom::List(args) => match args.first() { + //Some(Atom::Symbol("sample")) => { + //let (midi, sample) = MidiSample::from_atom((jack, &dir), &args[1..])?; + //if let Some(midi) = midi { + //samples.insert(midi, sample); + //} else { + //panic!("sample without midi binding: {}", sample.read().unwrap().name); + //} + //}, + //_ => panic!("unexpected in sampler {name}: {args:?}") + //}, + //_ => panic!("unexpected in sampler {name}: {atom:?}") + //}); + //Self::new(jack, &name) +//}); +//from_atom!("sample" => |(_jack, dir): (&Jack, &str), args| -> MidiSample { + //let mut name = String::new(); + //let mut file = String::new(); + //let mut midi = None; + //let mut start = 0usize; + //atom!(atom in args { + //Atom::Map(map) => { + //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { + //name = String::from(*n); + //} + //if let Some(Atom::Str(f)) = map.get(&Atom::Key(":file")) { + //file = String::from(*f); + //} + //if let Some(Atom::Int(i)) = map.get(&Atom::Key(":start")) { + //start = *i as usize; + //} + //if let Some(Atom::Int(m)) = map.get(&Atom::Key(":midi")) { + //midi = Some(u7::from(*m as u8)); + //} + //}, + //_ => panic!("unexpected in sample {name}"), + //}); + //let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?; + //Ok((midi, Arc::new(RwLock::new(crate::Sample { + //name, + //start, + //end, + //channels: data, + //rate: None, + //gain: 1.0 + //})))) +//}); diff --git a/crates/sampler/src/has_sampler.rs b/crates/sampler/src/has_sampler.rs deleted file mode 100644 index 1f2cc58c..00000000 --- a/crates/sampler/src/has_sampler.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::*; -pub trait HasSampler { - fn sampler (&self) -> &Option; - fn sampler_mut (&mut self) -> &mut Option; - fn sample_index (&self) -> usize; - fn view_sample <'a> (&'a self, compact: bool) -> impl Content + 'a { - self.sampler().as_ref().map(|sampler|Max::y( - if compact { 0u16 } else { 5 }.into(), - Fill::x(sampler.viewer(self.sample_index())) - )) - } - fn view_sampler <'a> (&'a self, compact: bool, editor: &Option) -> impl Content + 'a { - self.sampler().as_ref().map(|sampler|Fixed::x( - if compact { 4u16 } else { 40 }.into(), - Push::y( - if compact { 1u16 } else { 0 }.into(), - editor.as_ref().map(|e|Fill::y(sampler.list(compact, e))) - ) - )) - } -} -#[macro_export] macro_rules! has_sampler { - (|$self:ident:$Struct:ty| { sampler = $e0:expr; index = $e1:expr; }) => { - impl HasSampler for $Struct { - fn sampler (&$self) -> &Option { &$e0 } - fn sampler_mut (&mut $self) -> &mut Option { &mut $e0 } - fn sample_index (&$self) -> usize { $e1 } - } - } -} diff --git a/crates/sampler/src/lib.rs b/crates/sampler/src/lib.rs index 8f7fba28..8229812b 100644 --- a/crates/sampler/src/lib.rs +++ b/crates/sampler/src/lib.rs @@ -1,6 +1,3 @@ -#![allow(unused)] -#![allow(dead_code)] -mod sampler; pub use self::sampler::*; pub(crate) use ::tek_jack::{*, jack::*}; pub(crate) use ::tek_midi::{*, midly::{*, live::*, num::*}}; pub(crate) use ::tengri::{dsl::*, input::*, output::*, tui::{*, ratatui::prelude::*}}; @@ -21,9 +18,16 @@ pub(crate) use symphonia::{ default::get_codecs, }; pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}}; + +mod sampler_api; pub use self::sampler_api::*; +mod sampler_data; pub use self::sampler_data::*; +mod sampler_audio; pub use self::sampler_audio::*; +mod sampler_browse; pub use self::sampler_browse::*; +mod sampler_midi; pub use self::sampler_midi::*; +mod sampler_model; pub use self::sampler_model::*; +mod sampler_view; pub use self::sampler_view::*; + #[cfg(test)] #[test] fn test_sampler () { // TODO! let sample = Sample::new("test", 0, 0, vec![]); } - -mod has_sampler; pub use self::has_sampler::*; diff --git a/crates/sampler/src/sampler.rs b/crates/sampler/src/sampler.rs deleted file mode 100644 index b3280a6a..00000000 --- a/crates/sampler/src/sampler.rs +++ /dev/null @@ -1,901 +0,0 @@ -use crate::*; -/// The sampler device plays sounds in response to MIDI notes. -#[derive(Debug)] pub struct Sampler { - pub jack: Jack, - pub name: String, - pub mapped: [Option>>;128], - pub recording: Option<(usize, Arc>)>, - pub unmapped: Vec>>, - pub voices: Arc>>, - pub midi_in: Option, - pub audio_ins: Vec, - pub input_meter: Vec, - pub audio_outs: Vec, - pub buffer: Vec>, - pub output_gain: f32 -} -/// A sound sample. -#[derive(Default, Debug)] pub struct Sample { - pub name: Arc, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, - pub gain: f32, -} -/// Load sample from WAV and assign to MIDI note. -#[macro_export] macro_rules! sample { - ($note:expr, $name:expr, $src:expr) => {{ - let (end, data) = read_sample_data($src)?; - ( - u7::from_int_lossy($note).into(), - Sample::new($name, 0, end, data).into() - ) - }}; -} -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} -impl Default for Sampler { - fn default () -> Self { - Self { - midi_in: None, - audio_ins: vec![], - input_meter: vec![0.0;2], - audio_outs: vec![], - jack: Default::default(), - name: "tek_sampler".to_string(), - mapped: [const { None };128], - unmapped: vec![], - voices: Arc::new(RwLock::new(vec![])), - buffer: vec![vec![0.0;16384];2], - output_gain: 1., - recording: None, - } - } -} -impl Sampler { - pub fn new ( - jack: &Jack, - name: impl AsRef, - midi_from: &[PortConnect], - audio_from: &[&[PortConnect];2], - audio_to: &[&[PortConnect];2], - ) -> Usually { - let name = name.as_ref(); - Ok(Self { - midi_in: Some(JackMidiIn::new(jack, format!("M/{name}"), midi_from)?), - audio_ins: vec![ - JackAudioIn::new(jack, &format!("L/{name}"), audio_from[0])?, - JackAudioIn::new(jack, &format!("R/{name}"), audio_from[1])?, - ], - audio_outs: vec![ - JackAudioOut::new(jack, &format!("{name}/L"), audio_to[0])?, - JackAudioOut::new(jack, &format!("{name}/R"), audio_to[1])?, - ], - ..Default::default() - }) - } - pub fn cancel_recording (&mut self) { - self.recording = None; - } - pub fn begin_recording (&mut self, index: usize) { - self.recording = Some(( - index, - Arc::new(RwLock::new(Sample::new("Sample", 0, 0, vec![vec![];self.audio_ins.len()]))) - )); - } - pub fn finish_recording (&mut self) -> Option>> { - let recording = self.recording.take(); - if let Some((index, sample)) = recording { - let old = self.mapped[index].clone(); - self.mapped[index] = Some(sample); - old - } 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 } - } - 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, - } - } - /// 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(()) - } - 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 */ } - _ => {} - } - } -} -audio!(|self: SamplerTui, client, scope|SamplerAudio(&mut self.state).process(client, scope)); -pub struct SamplerAudio<'a>(pub &'a mut Sampler); -audio!(|self: SamplerAudio<'a>, _client, scope|{ - self.0.process_midi_in(scope); - self.0.clear_output_buffer(); - self.0.process_audio_out(scope); - self.0.write_output_buffer(scope); - self.0.process_audio_in(scope); - Control::Continue -}); -impl Sampler { - pub fn process_audio_in (&mut self, scope: &ProcessScope) { - let Sampler { audio_ins, input_meter, recording, .. } = self; - if audio_ins.len() != input_meter.len() { - *input_meter = vec![0.0;audio_ins.len()]; - } - if let Some((_, sample)) = recording { - let mut sample = sample.write().unwrap(); - if sample.channels.len() != audio_ins.len() { - panic!("channel count mismatch"); - } - let iterator = audio_ins.iter().zip(input_meter).zip(sample.channels.iter_mut()); - let mut length = 0; - for ((input, meter), channel) in iterator { - let slice = input.port().as_slice(scope); - length = length.max(slice.len()); - let total: f32 = slice.iter().map(|x|x.abs()).sum(); - let count = slice.len() as f32; - *meter = 10. * (total / count).log10(); - channel.extend_from_slice(slice); - } - sample.end += length; - } else { - for (input, meter) in audio_ins.iter().zip(input_meter) { - let slice = input.port().as_slice(scope); - let total: f32 = slice.iter().map(|x|x.abs()).sum(); - let count = slice.len() as f32; - *meter = 10. * (total / count).log10(); - } - } - } - /// Create [Voice]s from [Sample]s in response to MIDI input. - pub fn process_midi_in (&mut self, scope: &ProcessScope) { - let Sampler { midi_in, mapped, voices, .. } = self; - if let Some(ref midi_in) = midi_in { - for RawMidi { time, bytes } in midi_in.port().iter(scope) { - if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { - match message { - MidiMessage::NoteOn { ref key, ref vel } => { - if let Some(ref sample) = mapped[key.as_int() as usize] { - voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); - } - }, - MidiMessage::Controller { controller, value } => { - // TODO - } - _ => {} - } - } - } - } - } - /// Zero the output buffer. - pub fn clear_output_buffer (&mut self) { - for buffer in self.buffer.iter_mut() { - buffer.fill(0.0); - } - } - /// Mix all currently playing samples into the output. - pub fn process_audio_out (&mut self, scope: &ProcessScope) { - let Sampler { ref mut buffer, voices, output_gain, .. } = self; - let channel_count = buffer.len(); - voices.write().unwrap().retain_mut(|voice|{ - for index in 0..scope.n_frames() as usize { - if let Some(frame) = voice.next() { - for (channel, sample) in frame.iter().enumerate() { - // Averaging mixer: - //self.buffer[channel % channel_count][index] = ( - //(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0 - //); - buffer[channel % channel_count][index] += sample * *output_gain; - } - } else { - return false - } - } - true - }); - } - /// Write output buffer to output ports. - pub fn write_output_buffer (&mut self, scope: &ProcessScope) { - let Sampler { ref mut 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); - } - } - } -} -/////////////////////////////////////////////////////////////////////////////////////////////////// -type MidiSample = (Option, Arc>); -//from_atom!("sampler" => |jack: &Jack, args| -> crate::Sampler { - //let mut name = String::new(); - //let mut dir = String::new(); - //let mut samples = BTreeMap::new(); - //atom!(atom in args { - //Atom::Map(map) => { - //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { - //name = String::from(*n); - //} - //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":dir")) { - //dir = String::from(*n); - //} - //}, - //Atom::List(args) => match args.first() { - //Some(Atom::Symbol("sample")) => { - //let (midi, sample) = MidiSample::from_atom((jack, &dir), &args[1..])?; - //if let Some(midi) = midi { - //samples.insert(midi, sample); - //} else { - //panic!("sample without midi binding: {}", sample.read().unwrap().name); - //} - //}, - //_ => panic!("unexpected in sampler {name}: {args:?}") - //}, - //_ => panic!("unexpected in sampler {name}: {atom:?}") - //}); - //Self::new(jack, &name) -//}); -//from_atom!("sample" => |(_jack, dir): (&Jack, &str), args| -> MidiSample { - //let mut name = String::new(); - //let mut file = String::new(); - //let mut midi = None; - //let mut start = 0usize; - //atom!(atom in args { - //Atom::Map(map) => { - //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { - //name = String::from(*n); - //} - //if let Some(Atom::Str(f)) = map.get(&Atom::Key(":file")) { - //file = String::from(*f); - //} - //if let Some(Atom::Int(i)) = map.get(&Atom::Key(":start")) { - //start = *i as usize; - //} - //if let Some(Atom::Int(m)) = map.get(&Atom::Key(":midi")) { - //midi = Some(u7::from(*m as u8)); - //} - //}, - //_ => panic!("unexpected in sample {name}"), - //}); - //let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?; - //Ok((midi, Arc::new(RwLock::new(crate::Sample { - //name, - //start, - //end, - //channels: data, - //rate: None, - //gain: 1.0 - //})))) -//}); -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 - } -} -pub struct AddSampleModal { - exited: bool, - dir: PathBuf, - subdirs: Vec, - files: Vec, - cursor: usize, - offset: usize, - sample: Arc>, - voices: Arc>>, - _search: Option, -} -impl AddSampleModal { - fn exited (&self) -> bool { - self.exited - } - fn exit (&mut self) { - self.exited = true - } -} -impl AddSampleModal { - 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) - } -} -fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { - todo!(); -} -fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { - let (mut subdirs, mut files) = std::fs::read_dir(dir)? - .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ - let entry = entry.expect("failed to read drectory entry"); - let meta = entry.metadata().expect("failed to read entry metadata"); - if meta.is_file() { - files.push(entry.file_name()); - } else if meta.is_dir() { - subdirs.push(entry.file_name()); - } - (subdirs, files) - }); - subdirs.sort(); - files.sort(); - Ok((subdirs, files)) -} -fn draw_sample ( - to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool -) -> Usually { - let style = if focus { Style::default().green() } else { Style::default() }; - if focus { - to.blit(&"🬴", x+1, y, Some(style.bold())); - } - let label1 = format!("{:3} {:12}", - note.map(|n|n.to_string()).unwrap_or(String::default()), - sample.name); - let label2 = format!("{:>6} {:>6} +0.0", - sample.start, - sample.end); - to.blit(&label1, x+2, y, Some(style.bold())); - to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); - Ok(label1.len() + label2.len() + 4) -} -impl Content for AddSampleModal { - fn render (&self, to: &mut TuiOut) { - todo!() - //let area = to.area(); - //to.make_dim(); - //let area = center_box( - //area, - //64.max(area.w().saturating_sub(8)), - //20.max(area.w().saturating_sub(8)), - //); - //to.fill_fg(area, Color::Reset); - //to.fill_bg(area, Nord::bg_lo(true, true)); - //to.fill_char(area, ' '); - //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; - //to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?; - //for (i, (is_dir, name)) in self.subdirs.iter() - //.map(|path|(true, path)) - //.chain(self.files.iter().map(|path|(false, path))) - //.enumerate() - //.skip(self.offset) - //{ - //if i >= area.h() as usize - 4 { - //break - //} - //let t = if is_dir { "" } else { "" }; - //let line = format!("{t} {}", name.to_string_lossy()); - //let line = &line[..line.len().min(area.w() as usize - 4)]; - //to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor { - //Style::default().green() - //} else { - //Style::default().white() - //}))?; - //} - //Lozenge(Style::default()).draw(to) - } -} -//impl Handle for AddSampleModal { - //fn handle (&mut self, from: &TuiIn) -> Perhaps { - //if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? { - //return Ok(Some(true)) - //} - //Ok(Some(true)) - //} -//} -//pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { - //[Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ - //modal.exit(); - //Ok(true) - //}], - //[Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{ - //modal.prev(); - //Ok(true) - //}], - //[Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{ - //modal.next(); - //Ok(true) - //}], - //[Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{ - //if modal.pick()? { - //modal.exit(); - //} - //Ok(true) - //}], - //[Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{ - //modal.try_preview()?; - //Ok(true) - //}] -//}); -pub enum SamplerMode { - // Load sample from path - Import(usize, FileBrowser), -} -//handle!(TuiIn: |self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input.event())); -#[derive(Clone, Debug)] pub enum SamplerTuiCommand { - Import(FileBrowserCommand), - Select(usize), - Sample(SamplerCommand), -} -atom_command!(SamplerTuiCommand: |state: SamplerTui| { - ("select" [i: usize] Some(Self::Select(i.expect("no index")))) - ("import" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Import)) - ("sample" [,..a] SamplerCommand::try_from_expr(&state.state, a).map(Self::Sample)) }); -provide!(usize: |self: SamplerTui| {}); -provide!(PathBuf: |self: SamplerTui| {}); -provide!(Arc: |self: SamplerTui| {}); -atom_command!(FileBrowserCommand: |state: SamplerTui| { - ("begin" [] Some(Self::Begin)) - ("cancel" [] Some(Self::Cancel)) - ("confirm" [] Some(Self::Confirm)) - ("select" [i: usize] Some(Self::Select(i.expect("no index")))) - ("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path")))) - ("filter" [f: Arc] Some(Self::Filter(f.expect("no filter")))) -}); -#[derive(Clone, Debug)] pub enum SamplerCommand { - RecordBegin(u7), - RecordCancel, - RecordFinish, - SetSample(u7, Option>>), - SetStart(u7, usize), - SetGain(u7, f32), - NoteOn(u7, u7), - NoteOff(u7), -} -atom_command!(SamplerCommand: |state: Sampler| { - ("record/begin" [i: u7] - Some(Self::RecordBegin(i.expect("no index")))) - ("record/cancel" [] - Some(Self::RecordCancel)) - ("record/finish" [] - Some(Self::RecordFinish)) - ("set/sample" [i: u7, s: Option>>] - Some(Self::SetSample(i.expect("no index"), s.expect("no sampler")))) - ("set/start" [i: u7, s: usize] - Some(Self::SetStart(i.expect("no index"), s.expect("no start")))) - ("set/gain" [i: u7, g: f32] - Some(Self::SetGain(i.expect("no index"), g.expect("no garin")))) - ("note/on" [p: u7, v: u7] - Some(Self::NoteOn(p.expect("no pitch"), v.expect("no velocity")))) - ("note/off" [p: u7] - Some(Self::NoteOff(p.expect("no pitch")))) -}); -provide!(u7: |self: Sampler| {}); -provide!(Option>>: |self: Sampler| {}); -provide!(usize: |self: Sampler| {}); -provide!(f32: |self: Sampler| {}); -//input_to_command!(FileBrowserCommand: |state:SamplerTui, input: Event|match input { _ => return None }); -command!(|self: FileBrowserCommand,state:SamplerTui|match self { _ => todo!() }); -//input_to_command!(SamplerTuiCommand: |state: SamplerTui, input: Event|match state.mode{ - //Some(SamplerMode::Import(..)) => Self::Import( - //FileBrowserCommand::input_to_command(state, input)? - //), - //_ => match input { - //// load sample - //kpat!(Shift-Char('L')) => Self::Import(FileBrowserCommand::Begin), - //kpat!(KeyCode::Up) => Self::Select(state.note_pos().overflowing_add(1).0.min(127)), - //kpat!(KeyCode::Down) => Self::Select(state.note_pos().overflowing_sub(1).0.min(127)), - //_ => return None - //} -//}); -command!(|self: SamplerTuiCommand, state: SamplerTui|match self { - Self::Import(FileBrowserCommand::Begin) => { - //let voices = &state.state.voices; - //let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); - None - }, - Self::Select(index) => { - let old = state.note_pos(); - state.set_note_pos(index); - Some(Self::Select(old)) - }, - Self::Sample(cmd) => cmd.execute(&mut state.state)?.map(Self::Sample), - _ => todo!("{self:?}") -}); -command!(|self: SamplerCommand, state: Sampler|match self { - Self::RecordBegin(index) => { state.begin_recording(index.as_int() as usize); None }, - Self::RecordCancel => { state.cancel_recording(); None }, - Self::RecordFinish => { state.finish_recording(); None }, - Self::SetSample(index, sample) => { - let i = index.as_int() as usize; - let old = state.mapped[i].clone(); - state.mapped[i] = sample; - Some(Self::SetSample(index, old)) - }, - _ => todo!("{self:?}") -}); -pub struct SamplerTui { - pub state: Sampler, - pub cursor: (usize, usize), - pub editing: Option>>, - pub mode: Option, - /// Size of actual notes area - pub size: Measure, - /// Lowest note displayed - pub note_lo: AtomicUsize, - pub note_pt: AtomicUsize, - pub color: ItemPalette -} -impl SamplerTui { - /// Immutable reference to sample at cursor. - pub fn sample (&self) -> Option<&Arc>> { - for (i, sample) in self.state.mapped.iter().enumerate() { - if i == self.cursor.0 { - return sample.as_ref() - } - } - for (i, sample) in self.state.unmapped.iter().enumerate() { - if i + self.state.mapped.len() == self.cursor.0 { - return Some(sample) - } - } - None - } -} -content!(TuiOut: |self: SamplerTui| { - let keys_width = 5; - let keys = move||"";//SamplerKeys(self); - let fg = self.color.base.rgb; - let bg = self.color.darkest.rgb; - let border = Fill::xy(Outer(true, Style::default().fg(fg).bg(bg))); - let with_border = |x|lay!(border, Fill::xy(x)); - let with_size = |x|lay!(self.size.clone(), x); - Tui::bg(bg, Fill::xy(with_border(Bsp::s( - Tui::fg(self.color.light.rgb, Tui::bold(true, &"Sampler")), - with_size(Shrink::y(1, Bsp::e( - Fixed::x(keys_width, keys()), - Fill::xy(SamplesTui { - color: self.color, - note_hi: self.note_hi(), - note_pt: self.note_pos(), - height: self.size.h(), - }), - ))), - )))) -}); -struct SamplesTui { - color: ItemPalette, - note_hi: usize, - note_pt: usize, - height: usize, -} -render!(TuiOut: |self: SamplesTui, to| { - let x = to.area.x(); - let bg_base = self.color.darkest.rgb; - let bg_selected = self.color.darker.rgb; - let style_empty = Style::default().fg(self.color.base.rgb); - let style_full = Style::default().fg(self.color.lighter.rgb); - for y in 0..self.height { - let note = self.note_hi - y as usize; - let bg = if note == self.note_pt { bg_selected } else { bg_base }; - let style = Some(style_empty.bg(bg)); - to.blit(&" (no sample) ", x, to.area.y() + y as u16, style); - } -}); -impl NoteRange for SamplerTui { - fn note_lo (&self) -> &AtomicUsize { &self.note_lo } - fn note_axis (&self) -> &AtomicUsize { &self.size.y } -} -impl NotePoint for SamplerTui { - fn note_len (&self) -> usize {0/*TODO*/} - fn set_note_len (&self, x: usize) {} - fn note_pos (&self) -> usize { self.note_pt.load(Relaxed) } - fn set_note_pos (&self, x: usize) { self.note_pt.store(x, Relaxed); } -} -impl Sampler { - const _EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; - pub fn list <'a> (&'a self, compact: bool, editor: &MidiEditor) -> impl Content + 'a { - let note_lo = editor.note_lo().load(Relaxed); - let note_pt = editor.note_pos(); - let note_hi = editor.note_hi(); - Outer(true, Style::default().fg(Tui::g(96))).enclose(Map::new(move||(note_lo..=note_hi).rev(), move|note, i| { - let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a)))); - let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset }; - let mut fg = Tui::g(160); - let mapped: &Option>> = &self.mapped[note]; - if mapped.is_some() { - fg = Tui::g(224); - bg = Color::Rgb(0, if note == note_pt { 96 } else { 64 }, 0); - } - if let Some((index, _)) = self.recording { - if note == index { - bg = if note == note_pt { Color::Rgb(96,24,0) } else { Color::Rgb(64,16,0) }; - fg = Color::Rgb(224,64,32) - } - } - offset(Tui::fg_bg(fg, bg, format!("{note:3} {}", self.list_item(note, compact)))) - })) - } - pub fn list_item (&self, note: usize, compact: bool) -> String { - if compact { - String::default() - } else if let Some(sample) = &self.mapped[note] { - let sample = sample.read().unwrap(); - format!("{:8} {:3} {:6}-{:6}/{:6}", - sample.name, - sample.gain, - sample.start, - sample.end, - sample.channels[0].len() - ) - } else { - String::from("(none)") - } - } - pub fn viewer (&self, note_pt: usize) -> impl Content { - let sample = if let Some((_, sample)) = &self.recording { - Some(sample.clone()) - } else if let Some(sample) = &self.mapped[note_pt] { - Some(sample.clone()) - } else { - None - }; - let min_db = -40.0; - ThunkRender::new(move|to: &mut TuiOut|{ - let [x, y, width, height] = to.area(); - let area = Rect { x, y, width, height }; - let (x_bounds, y_bounds, lines): ([f64;2], [f64;2], Vec) = - 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.; - } - ( - [sample.start as f64, sample.end as f64], - [min_db, 0.], - lines - ) - } else { - ( - [0.0, width as f64], - [0.0, height as f64], - vec![ - Line::new(0.0, 0.0, width as f64, height as f64, Color::Red), - Line::new(width as f64, 0.0, 0.0, height as f64, Color::Red), - ] - ) - }; - Canvas::default() - .x_bounds(x_bounds) - .y_bounds(y_bounds) - .paint(|ctx| { for line in lines.iter() { ctx.draw(line) } }) - .render(area, &mut to.buffer); - }) - } - pub fn status (&self, index: usize) -> impl Content { - Tui::bold(true, Tui::fg(Tui::g(224), self.mapped[index].as_ref().map(|sample|format!( - "Sample {}-{}", - sample.read().unwrap().start, - sample.read().unwrap().end, - )).unwrap_or_else(||"No sample".to_string()))) - } -} diff --git a/crates/sampler/src/sampler_api.rs b/crates/sampler/src/sampler_api.rs new file mode 100644 index 00000000..e1451cc1 --- /dev/null +++ b/crates/sampler/src/sampler_api.rs @@ -0,0 +1,119 @@ +use crate::*; +provide!(Arc: |self: Sampler| {}); +provide!(Option>>: |self: Sampler| {}); +provide!(PathBuf: |self: Sampler| {}); +provide!(f32: |self: Sampler| {}); +provide!(u7: |self: Sampler| {}); +provide!(usize: |self: Sampler| {}); + +//handle!(TuiIn: |self: Sampler, input|SamplerCommand::execute_with_state(self, input.event())); +//input_to_command!(SamplerCommand: |state: Sampler, input: Event|match state.mode{ + //Some(SamplerMode::Import(..)) => Self::Import( + //FileBrowserCommand::input_to_command(state, input)? + //), + //_ => match input { + //// load sample + //kpat!(Shift-Char('L')) => Self::Import(FileBrowserCommand::Begin), + //kpat!(KeyCode::Up) => Self::Select(state.note_pos().overflowing_add(1).0.min(127)), + //kpat!(KeyCode::Down) => Self::Select(state.note_pos().overflowing_sub(1).0.min(127)), + //_ => return None + //} +//}); + +defcom! { |self, state: Sampler| + + SamplerCommand { + + Import(cmd: FileBrowserCommand) => match cmd { + FileBrowserCommand::Begin => { + //let voices = &state.state.voices; + //let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); + None + }, + _ => { + println!("\n\rtodo: import: filebrowser: {cmd:?}"); + None + } + } + + Select(index: usize) => { + let old = state.note_pos(); + state.set_note_pos(index); + Some(Self::Select(old)) + } + + RecordBegin(pitch: u7) => { + state.begin_recording(pitch.as_int() as usize); + None + } + + RecordCancel => { + state.cancel_recording(); + None + } + + RecordFinish => { + state.finish_recording(); + None + } + + SetSample(pitch: u7, sample: Option>>) => { + let i = pitch.as_int() as usize; + let old = state.mapped[i].clone(); + state.mapped[i] = sample; + Some(Self::SetSample(pitch, old)) + } + + SetStart(pitch: u7, frame: usize) => { + println!("\n\rtodo: {self:?}"); + None + } + + SetGain(pitch: u7, gain: f32) => { + println!("\n\rtodo: {self:?}"); + None + } + + NoteOn(pitch: u7, velocity: u7) => { + println!("\n\rtodo: {self:?}"); + None + } + + NoteOff(pitch: u7) => { + println!("\n\rtodo: {self:?}"); + None + } + + } + +} + +atom_command!(FileBrowserCommand: |state: Sampler| { + ("begin" [] Some(Self::Begin)) + ("cancel" [] Some(Self::Cancel)) + ("confirm" [] Some(Self::Confirm)) + ("select" [i: usize] Some(Self::Select(i.expect("no index")))) + ("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path")))) + ("filter" [f: Arc] Some(Self::Filter(f.expect("no filter")))) }); +atom_command!(SamplerCommand: |state: Sampler| { + ("select" [i: usize] + Some(Self::Select(i.expect("no index")))) + ("import" [,..a] + FileBrowserCommand::try_from_expr(state, a).map(Self::Import)) + ("record/begin" [i: u7] + Some(Self::RecordBegin(i.expect("no index")))) + ("record/cancel" [] + Some(Self::RecordCancel)) + ("record/finish" [] + Some(Self::RecordFinish)) + ("set/sample" [i: u7, s: Option>>] + Some(Self::SetSample(i.expect("no index"), s.expect("no sampler")))) + ("set/start" [i: u7, s: usize] + Some(Self::SetStart(i.expect("no index"), s.expect("no start")))) + ("set/gain" [i: u7, g: f32] + Some(Self::SetGain(i.expect("no index"), g.expect("no garin")))) + ("note/on" [p: u7, v: u7] + Some(Self::NoteOn(p.expect("no pitch"), v.expect("no velocity")))) + ("note/off" [p: u7] + Some(Self::NoteOff(p.expect("no pitch")))) }); diff --git a/crates/sampler/src/sampler_audio.rs b/crates/sampler/src/sampler_audio.rs new file mode 100644 index 00000000..4cea762c --- /dev/null +++ b/crates/sampler/src/sampler_audio.rs @@ -0,0 +1,107 @@ +use crate::*; + +pub struct SamplerAudio<'a>(pub &'a mut Sampler); + +audio!(|self: SamplerAudio<'a>, _client, scope|{ + self.0.process_midi_in(scope); + self.0.clear_output_buffer(); + self.0.process_audio_out(scope); + self.0.write_output_buffer(scope); + self.0.process_audio_in(scope); + Control::Continue +}); + +impl Sampler { + + pub fn process_audio_in (&mut self, scope: &ProcessScope) { + let Sampler { audio_ins, input_meter, recording, .. } = self; + if audio_ins.len() != input_meter.len() { + *input_meter = vec![0.0;audio_ins.len()]; + } + if let Some((_, sample)) = recording { + let mut sample = sample.write().unwrap(); + if sample.channels.len() != audio_ins.len() { + panic!("channel count mismatch"); + } + let iterator = audio_ins.iter().zip(input_meter).zip(sample.channels.iter_mut()); + let mut length = 0; + for ((input, meter), channel) in iterator { + let slice = input.port().as_slice(scope); + length = length.max(slice.len()); + let total: f32 = slice.iter().map(|x|x.abs()).sum(); + let count = slice.len() as f32; + *meter = 10. * (total / count).log10(); + channel.extend_from_slice(slice); + } + sample.end += length; + } else { + for (input, meter) in audio_ins.iter().zip(input_meter) { + let slice = input.port().as_slice(scope); + let total: f32 = slice.iter().map(|x|x.abs()).sum(); + let count = slice.len() as f32; + *meter = 10. * (total / count).log10(); + } + } + } + + /// Zero the output buffer. + pub fn clear_output_buffer (&mut self) { + for buffer in self.buffer.iter_mut() { + buffer.fill(0.0); + } + } + + /// Mix all currently playing samples into the output. + pub fn process_audio_out (&mut self, scope: &ProcessScope) { + let Sampler { ref mut buffer, voices, output_gain, .. } = self; + let channel_count = buffer.len(); + voices.write().unwrap().retain_mut(|voice|{ + for index in 0..scope.n_frames() as usize { + if let Some(frame) = voice.next() { + for (channel, sample) in frame.iter().enumerate() { + // Averaging mixer: + //self.buffer[channel % channel_count][index] = ( + //(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0 + //); + buffer[channel % channel_count][index] += sample * *output_gain; + } + } else { + return false + } + } + true + }); + } + + /// Write output buffer to output ports. + pub fn write_output_buffer (&mut self, scope: &ProcessScope) { + let Sampler { ref mut 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 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 + } +} diff --git a/crates/sampler/src/sampler_browse.rs b/crates/sampler/src/sampler_browse.rs new file mode 100644 index 00000000..0010a3da --- /dev/null +++ b/crates/sampler/src/sampler_browse.rs @@ -0,0 +1,181 @@ +use crate::*; + +pub struct AddSampleModal { + exited: bool, + dir: PathBuf, + subdirs: Vec, + files: Vec, + cursor: usize, + offset: usize, + sample: Arc>, + voices: Arc>>, + _search: Option, +} + +impl AddSampleModal { + fn exited (&self) -> bool { + self.exited + } + fn exit (&mut self) { + self.exited = true + } +} + +impl AddSampleModal { + 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) + } +} + +fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { + todo!(); +} + +fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { + let (mut subdirs, mut files) = std::fs::read_dir(dir)? + .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ + let entry = entry.expect("failed to read drectory entry"); + let meta = entry.metadata().expect("failed to read entry metadata"); + if meta.is_file() { + files.push(entry.file_name()); + } else if meta.is_dir() { + subdirs.push(entry.file_name()); + } + (subdirs, files) + }); + subdirs.sort(); + files.sort(); + Ok((subdirs, files)) +} + +impl Content for AddSampleModal { + fn render (&self, to: &mut TuiOut) { + todo!() + //let area = to.area(); + //to.make_dim(); + //let area = center_box( + //area, + //64.max(area.w().saturating_sub(8)), + //20.max(area.w().saturating_sub(8)), + //); + //to.fill_fg(area, Color::Reset); + //to.fill_bg(area, Nord::bg_lo(true, true)); + //to.fill_char(area, ' '); + //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; + //to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?; + //for (i, (is_dir, name)) in self.subdirs.iter() + //.map(|path|(true, path)) + //.chain(self.files.iter().map(|path|(false, path))) + //.enumerate() + //.skip(self.offset) + //{ + //if i >= area.h() as usize - 4 { + //break + //} + //let t = if is_dir { "" } else { "" }; + //let line = format!("{t} {}", name.to_string_lossy()); + //let line = &line[..line.len().min(area.w() as usize - 4)]; + //to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor { + //Style::default().green() + //} else { + //Style::default().white() + //}))?; + //} + //Lozenge(Style::default()).draw(to) + } +} diff --git a/crates/sampler/src/sampler_data.rs b/crates/sampler/src/sampler_data.rs new file mode 100644 index 00000000..8c3b3bda --- /dev/null +++ b/crates/sampler/src/sampler_data.rs @@ -0,0 +1,87 @@ +use crate::*; + +impl Sample { + /// 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(()) + } +} diff --git a/crates/sampler/src/sampler_midi.rs b/crates/sampler/src/sampler_midi.rs new file mode 100644 index 00000000..2fd7b3a4 --- /dev/null +++ b/crates/sampler/src/sampler_midi.rs @@ -0,0 +1,54 @@ +use crate::*; + +pub type MidiSample = (Option, Arc>); + +impl Sampler { + + /// Create [Voice]s from [Sample]s in response to MIDI input. + pub fn process_midi_in (&mut self, scope: &ProcessScope) { + let Sampler { midi_in, mapped, voices, .. } = self; + if let Some(ref midi_in) = midi_in { + for RawMidi { time, bytes } in midi_in.port().iter(scope) { + if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { + match message { + MidiMessage::NoteOn { ref key, ref vel } => { + if let Some(ref sample) = mapped[key.as_int() as usize] { + voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); + } + }, + MidiMessage::Controller { controller, value } => { + // TODO + } + _ => {} + } + } + } + } + } + +} + +impl Sample { + 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 */ } + _ => {} + } + } +} diff --git a/crates/sampler/src/sampler_model.rs b/crates/sampler/src/sampler_model.rs new file mode 100644 index 00000000..90236a2d --- /dev/null +++ b/crates/sampler/src/sampler_model.rs @@ -0,0 +1,149 @@ +use crate::*; + +/// The sampler device plays sounds in response to MIDI notes. +#[derive(Debug)] +pub struct Sampler { + pub name: String, + pub mapped: [Option>>;128], + pub recording: Option<(usize, Arc>)>, + pub unmapped: Vec>>, + pub voices: Arc>>, + pub midi_in: Option, + pub audio_ins: Vec, + pub input_meter: Vec, + pub audio_outs: Vec, + pub buffer: Vec>, + pub output_gain: f32, + pub cursor: (usize, usize), + pub editing: Option>>, + pub mode: Option, + /// Size of actual notes area + pub size: Measure, + /// Lowest note displayed + pub note_lo: AtomicUsize, + pub note_pt: AtomicUsize, + pub color: ItemPalette +} + +impl Default for Sampler { + fn default () -> Self { + Self { + midi_in: None, + audio_ins: vec![], + input_meter: vec![0.0;2], + audio_outs: vec![], + name: "tek_sampler".to_string(), + mapped: [const { None };128], + unmapped: vec![], + voices: Arc::new(RwLock::new(vec![])), + buffer: vec![vec![0.0;16384];2], + output_gain: 1., + recording: None, + mode: None, + cursor: (0, 0), + editing: None, + size: Default::default(), + note_lo: 0.into(), + note_pt: 0.into(), + color: Default::default(), + } + } +} + +impl Sampler { + pub fn new ( + jack: &Jack, + name: impl AsRef, + midi_from: &[PortConnect], + audio_from: &[&[PortConnect];2], + audio_to: &[&[PortConnect];2], + ) -> Usually { + let name = name.as_ref(); + Ok(Self { + midi_in: Some(JackMidiIn::new(jack, format!("M/{name}"), midi_from)?), + audio_ins: vec![ + JackAudioIn::new(jack, &format!("L/{name}"), audio_from[0])?, + JackAudioIn::new(jack, &format!("R/{name}"), audio_from[1])?, + ], + audio_outs: vec![ + JackAudioOut::new(jack, &format!("{name}/L"), audio_to[0])?, + JackAudioOut::new(jack, &format!("{name}/R"), audio_to[1])?, + ], + ..Default::default() + }) + } + pub fn cancel_recording (&mut self) { + self.recording = None; + } + pub fn begin_recording (&mut self, index: usize) { + self.recording = Some(( + index, + Arc::new(RwLock::new(Sample::new("Sample", 0, 0, vec![vec![];self.audio_ins.len()]))) + )); + } + pub fn finish_recording (&mut self) -> Option>> { + let recording = self.recording.take(); + if let Some((index, sample)) = recording { + let old = self.mapped[index].clone(); + self.mapped[index] = Some(sample); + old + } else { + None + } + } + /// Immutable reference to sample at cursor. + pub fn sample (&self) -> Option<&Arc>> { + for (i, sample) in self.mapped.iter().enumerate() { + if i == self.cursor.0 { + return sample.as_ref() + } + } + for (i, sample) in self.unmapped.iter().enumerate() { + if i + self.mapped.len() == self.cursor.0 { + return Some(sample) + } + } + None + } +} + +/// A sound sample. +#[derive(Default, Debug)] +pub struct Sample { + pub name: Arc, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, + pub gain: f32, +} + +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 } + } + 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, + } + } +} + +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] +pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} + +#[derive(Debug)] +pub enum SamplerMode { + // Load sample from path + Import(usize, FileBrowser), +} + diff --git a/crates/sampler/src/sampler_view.rs b/crates/sampler/src/sampler_view.rs new file mode 100644 index 00000000..a39c7193 --- /dev/null +++ b/crates/sampler/src/sampler_view.rs @@ -0,0 +1,192 @@ +use crate::*; + +content!(TuiOut: |self: Sampler| { + let keys_width = 5; + let keys = move||"";//SamplerKeys(self); + let fg = self.color.base.rgb; + let bg = self.color.darkest.rgb; + let border = Fill::xy(Outer(true, Style::default().fg(fg).bg(bg))); + let with_border = |x|lay!(border, Fill::xy(x)); + let with_size = |x|lay!(self.size.clone(), x); + Tui::bg(bg, Fill::xy(with_border(Bsp::s( + Tui::fg(self.color.light.rgb, Tui::bold(true, &"Sampler")), + with_size(Shrink::y(1, Bsp::e( + Fixed::x(keys_width, keys()), + Fill::xy(SamplesTui { + color: self.color, + note_hi: self.note_hi(), + note_pt: self.note_pos(), + height: self.size.h(), + }), + ))), + )))) +}); + +struct SamplesTui { + color: ItemPalette, + note_hi: usize, + note_pt: usize, + height: usize, +} + +render!(TuiOut: |self: SamplesTui, to| { + let x = to.area.x(); + let bg_base = self.color.darkest.rgb; + let bg_selected = self.color.darker.rgb; + let style_empty = Style::default().fg(self.color.base.rgb); + let style_full = Style::default().fg(self.color.lighter.rgb); + for y in 0..self.height { + let note = self.note_hi - y as usize; + let bg = if note == self.note_pt { bg_selected } else { bg_base }; + let style = Some(style_empty.bg(bg)); + to.blit(&" (no sample) ", x, to.area.y() + y as u16, style); + } +}); + +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) -> usize {0/*TODO*/} + fn set_note_len (&self, x: usize) {} + fn note_pos (&self) -> usize { self.note_pt.load(Relaxed) } + fn set_note_pos (&self, x: usize) { self.note_pt.store(x, Relaxed); } +} + +impl Sampler { + const _EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; + pub fn list <'a> (&'a self, compact: bool, editor: &MidiEditor) -> impl Content + 'a { + let note_lo = editor.note_lo().load(Relaxed); + let note_pt = editor.note_pos(); + let note_hi = editor.note_hi(); + Outer(true, Style::default().fg(Tui::g(96))).enclose(Map::new( + move||(note_lo..=note_hi).rev(), + move|note, i| { + let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a)))); + let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset }; + let mut fg = Tui::g(160); + let mapped: &Option>> = &self.mapped[note]; + if mapped.is_some() { + fg = Tui::g(224); + bg = Color::Rgb(0, if note == note_pt { 96 } else { 64 }, 0); + } + if let Some((index, _)) = self.recording { + if note == index { + bg = if note == note_pt { Color::Rgb(96,24,0) } else { Color::Rgb(64,16,0) }; + fg = Color::Rgb(224,64,32) + } + } + offset(Tui::fg_bg(fg, bg, format!("{note:3} {}", self.list_item(note, compact)))) + })) + } + pub fn list_item (&self, note: usize, compact: bool) -> String { + if compact { + String::default() + } else { + draw_list_item(&self.mapped[note]) + } + } + pub fn viewer (&self, note_pt: usize) -> impl Content + use<'_> { + draw_viewer(if let Some((_, sample)) = &self.recording { + Some(sample) + } else if let Some(sample) = &self.mapped[note_pt] { + Some(sample) + } else { + None + }) + } + pub fn status (&self, index: usize) -> impl Content { + draw_status(self.mapped[index].as_ref()) + } +} + +fn draw_list_item (sample: &Option>>) -> String { + if let Some(sample) = sample { + let sample = sample.read().unwrap(); + format!("{:8} {:3} {:6}-{:6}/{:6}", + sample.name, + sample.gain, + sample.start, + sample.end, + sample.channels[0].len() + ) + } else { + String::from("(none)") + } +} + +fn draw_viewer (sample: Option<&Arc>>) -> impl Content + use<'_> { + let min_db = -40.0; + ThunkRender::new(move|to: &mut TuiOut|{ + let [x, y, width, height] = to.area(); + let area = Rect { x, y, width, height }; + let (x_bounds, y_bounds, lines): ([f64;2], [f64;2], Vec) = + 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.; + } + ( + [sample.start as f64, sample.end as f64], + [min_db, 0.], + lines + ) + } else { + ( + [0.0, width as f64], + [0.0, height as f64], + vec![ + Line::new(0.0, 0.0, width as f64, height as f64, Color::Red), + Line::new(width as f64, 0.0, 0.0, height as f64, Color::Red), + ] + ) + }; + Canvas::default() + .x_bounds(x_bounds) + .y_bounds(y_bounds) + .paint(|ctx| { for line in lines.iter() { ctx.draw(line) } }) + .render(area, &mut to.buffer); + }) +} + +fn draw_status (sample: Option<&Arc>>) -> impl Content { + 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()))) +} + +fn draw_sample ( + to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool +) -> Usually { + let style = if focus { Style::default().green() } else { Style::default() }; + if focus { + to.blit(&"🬴", x+1, y, Some(style.bold())); + } + let label1 = format!("{:3} {:12}", + note.map(|n|n.to_string()).unwrap_or(String::default()), + sample.name); + let label2 = format!("{:>6} {:>6} +0.0", + sample.start, + sample.end); + to.blit(&label1, x+2, y, Some(style.bold())); + to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); + Ok(label1.len() + label2.len() + 4) +} diff --git a/deps/tengri b/deps/tengri index dd42a991..4e811f63 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit dd42a99186440ffec3ad975c971241482dd4188a +Subproject commit 4e811f63f149c662e0c3ed25138475b9a2c802ac