diff --git a/src/conf.rs b/src/conf.rs index 1ceddf9a..86b0fa80 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -8,6 +8,7 @@ impl_as_mut!(Config: |self: ConfigWatcher| &mut self.config); impl_as_ref!(ConfigWatcher: |self: ConfigWatcher| &self); mod bind; pub use self::bind::*; +mod lang; pub use self::lang::*; mod mode; pub use self::mode::*; mod view; pub use self::view::*; diff --git a/src/conf/lang.rs b/src/conf/lang.rs new file mode 100644 index 00000000..36e3aa31 --- /dev/null +++ b/src/conf/lang.rs @@ -0,0 +1,154 @@ +use crate::*; + +pub fn tek_arc_str <'a> (_: &App, src: impl Language) -> Perhaps> { + Ok(src.src()?.map(|x|x.into())) +} + +pub fn tek_u16 <'a> (state: &App, src: impl Language) -> Perhaps { + Ok(Some(match src.word()? { + Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()), + Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9), + _ => return try_to_u16(src) + })) +} + +pub fn tek_usize <'a> (state: &App, src: impl Language) -> Perhaps { + Ok(Some(match src.word()? { + Some(":scene-count") => state.scenes().len(), + Some(":track-count") => state.tracks().len(), + Some(":device-kind") => state.dialog.device_kind().unwrap_or(0), + Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0), + Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0), + _ => return try_to_usize(src) + })) +} + +pub fn tek_f64 <'a> (_state: &App, _src: impl Language) -> Perhaps { + todo!() +} + +pub fn tek_bool <'a> (state: &App, src: impl Language) -> Perhaps { + src.word()?.map(|word|Ok(match word { + "Y" => true, + "N" => false, + ":mode/editor" => state.project.editor.is_some(), + ":focused/dialog" => !matches!(state.dialog, Dialog::None), + ":focused/message" => matches!(state.dialog, Dialog::Message(..)), + ":focused/add_device" => matches!(state.dialog, Dialog::Device(..)), + ":focused/browser" => state.dialog.browser().is_some(), + //":focused/pool/import" => matches!(state.project.pool.mode, Some(PoolMode::Import(..))), + //":focused/pool/export" => matches!(state.project.pool.mode, Some(PoolMode::Export(..))), + //":focused/pool/rename" => matches!(state.project.pool.mode, Some(PoolMode::Rename(..))), + //":focused/pool/length" => matches!(state.project.pool.mode, Some(PoolMode::Length(..))), + ":focused/clip" => matches!(state.selection(), Selection::TrackClip{..}), + ":focused/track" => matches!(state.selection(), Selection::Track(..)), + ":focused/scene" => matches!(state.selection(), Selection::Scene(..)), + ":focused/mix" => matches!(state.selection(), Selection::Mix), + _ => return err!("not bool: {word}") + })).transpose() +} + +#[allow(unused)] +pub fn tek_selection <'a> (state: &App, src: impl Language) -> Perhaps { + src.word()?.map(|word|Ok(match word { + ":select/scene" => state.selection().select_scene(state.tracks().len()), + ":select/scene/next" => state.selection().select_scene_next(state.scenes().len()), + ":select/scene/prev" => state.selection().select_scene_prev(), + ":select/track" => state.selection().select_track(state.tracks().len()), + ":select/track/next" => state.selection().select_track_next(state.tracks().len()), + ":select/track/prev" => state.selection().select_track_prev(), + _ => return err!("not selection: {word}") + })).transpose() +} + +pub fn tek_color <'a> (_: &App, src: impl Language) -> Perhaps { + if let Some(expr) = src.expr()? { + match (expr.head()?, expr.tail()?) { + (Some("g"), Some(tail)) => { + let n = try_to_u8(tail.head().map_err(Into::into))? + .ok_or(LanguageError::domain("not gray"))?; + Ok(Some(Color::Rgb(n, n, n))) + }, + (Some("rgb"), Some(tail)) => { + let r = try_to_u8(tail.head().map_err(Into::into))? + .ok_or(LanguageError::domain("not red"))?; + let g = try_to_u8(tail.tail().head().map_err(Into::into))? + .ok_or(LanguageError::domain("not green"))?; + let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))? + .ok_or(LanguageError::domain("not blue"))?; + Ok(Some(Color::Rgb(r, g, b))) + }, + (Some(_), _) => return err!("not a color expression: {expr}"), + (None, _) => return err!("not a color expression: {expr}"), + } + } else if let Ok(Some(sym)) = src.word() { + Ok(match sym { + ":color/bg" => Some(Color::Rgb(28, 32, 36)), + ":color/fg" => Some(Color::Rgb(98, 92, 96)), + _ => return err!("not a color: {sym}") + }) + } else { + return err!("not a color: {:?}", src.src()?) + } +} + +#[allow(unused)] +pub fn tek_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps> { + eval_word(src, |word|Ok(match word { + + ":editor/pitch" => Some(( + state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8 + ).into()), + + _ => return err!("unknown midi note: {word}") + + })) +} + +pub fn tek_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps> { + eval_word(src, |word|Ok(match word { + + ":selected/scene" => state.selection().scene(), + ":selected/track" => state.selection().track(), + + _ => return err!("unknown opt: {word}") + + })) +} + + +pub fn tek_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps> { + Ok(None) +} + +pub fn tek_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps> { + Ok(None) +} + +pub fn tek_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps> { + todo!() +} + +pub fn tek_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps>> { + todo!() +} + +pub fn tek_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps> { + todo!() +} + +pub fn tek_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps>> { + todo!() +} + +pub fn tek_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps> { + todo!() +} + +pub fn tek_path_buf <'a> (_: &App, _: impl Language) -> Perhaps { + todo!() +} + +pub fn tek_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps>> { + todo!() +} diff --git a/src/conf/mode.rs b/src/conf/mode.rs index 86f692f8..5b1717ea 100644 --- a/src/conf/mode.rs +++ b/src/conf/mode.rs @@ -115,7 +115,7 @@ impl Mode { } impl ControlAxis { - pub fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps { + pub fn eval <'a> (_: &App, src: impl Language) -> Perhaps { eval_word(src, |word|Ok(ControlAxis(word.into()))) } } diff --git a/src/device/clap.rs b/src/gear/clap.rs similarity index 100% rename from src/device/clap.rs rename to src/gear/clap.rs diff --git a/src/device/lv2.rs b/src/gear/lv2.rs similarity index 100% rename from src/device/lv2.rs rename to src/gear/lv2.rs diff --git a/src/device/lv2_gui.rs b/src/gear/lv2_gui.rs similarity index 100% rename from src/device/lv2_gui.rs rename to src/gear/lv2_gui.rs diff --git a/src/sequence.rs b/src/gear/midi.rs similarity index 100% rename from src/sequence.rs rename to src/gear/midi.rs diff --git a/src/device/mixer.rs b/src/gear/mixer.rs similarity index 99% rename from src/device/mixer.rs rename to src/gear/mixer.rs index 7a0f2e51..86d0c75d 100644 --- a/src/device/mixer.rs +++ b/src/gear/mixer.rs @@ -1,6 +1,5 @@ use crate::*; -def_sizes_iter!(TracksSizes => Track); impl_as_mut!(Vec: |self: App| self.project.as_mut()); impl_as_ref!(Vec: |self: App| self.project.as_ref()); impl_has!(Sequencer: |self: Track| self.sequencer); diff --git a/src/pool.rs b/src/gear/pool.rs similarity index 100% rename from src/pool.rs rename to src/gear/pool.rs diff --git a/src/device/sample.rs b/src/gear/sample.rs similarity index 100% rename from src/device/sample.rs rename to src/gear/sample.rs diff --git a/src/device/vst2.rs b/src/gear/vst2.rs similarity index 100% rename from src/device/vst2.rs rename to src/gear/vst2.rs diff --git a/src/device/vst3.rs b/src/gear/vst3.rs similarity index 100% rename from src/device/vst3.rs rename to src/gear/vst3.rs diff --git a/src/project.rs b/src/project.rs deleted file mode 100644 index 097d13a3..00000000 --- a/src/project.rs +++ /dev/null @@ -1,594 +0,0 @@ -use crate::*; - -def_sizes_iter!(PortsSizes => Arc, [Connect]); -def_sizes_iter!(ScenesSizes => Scene); -impl_as_mut!(Option: |self: Project| &mut self.editor); -impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut()); -impl_as_mut_opt!(Track: |self: Project| self.selected_track_mut()); -impl_as_ref!(Option: |self: Project| &self.editor); -impl_as_ref_opt!(Scene: |self: Project| self.selected_scene()); -impl_as_ref_opt!(Track: |self: Project| self.selected_track()); -impl_has!(Clock: |self: Project| self.clock); -impl_has!(Jack<'static>: |self: Project| self.jack); -impl_has!(Selection: |self: Project| self.selection); -impl_has!(Sizer: |self: Project| self.size); -impl_has!(Vec: |self: Project| self.midi_ins); -impl_has!(Vec: |self: Project| self.midi_outs); - -#[cfg(feature = "cli")] pub use self::cli::*; -#[cfg(feature = "cli")] mod cli { - use super::*; - - /// Command to launch into new project from CLI. - #[derive(Debug, Clone, Parser, Default)] - pub struct ProjectInit { - /// Name of JACK client - #[arg(short='n', long)] name: Option, - /// Whether to attempt to become transport master - #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, - /// Whether to sync to external transport master - #[arg(short='y', long, default_value_t = true)] sync_follow: bool, - /// Initial tempo in beats per minute - #[arg(short='b', long, default_value = None)] bpm: Option, - /// Whether to include a transport toolbar (default: true) - #[arg(short='c', long, default_value_t = true)] show_clock: bool, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='I', long)] midi_from: Vec, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='i', long)] midi_from_re: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='O', long)] midi_to: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='o', long)] midi_to_re: Vec, - /// Audio outs to connect to left input - #[arg(short='l', long)] left_from: Vec, - /// Audio outs to connect to right input - #[arg(short='r', long)] right_from: Vec, - /// Audio ins to connect from left output - #[arg(short='L', long)] left_to: Vec, - /// Audio ins to connect from right output - #[arg(short='R', long)] right_to: Vec, - /// Tracks to creat - #[arg(short='t', long)] tracks: Option, - /// Scenes to create - #[arg(short='s', long)] scenes: Option, - } - - impl ProjectInit { - pub fn init (&self) -> Usually { - let name = self.name.as_ref().map_or("tek", |x|x.as_str()); - let jack = Jack::new(&name)?; - let midi_ins = connect_midi_ins(&jack, &"M".to_string(), &self.midi_from, Some(&self.midi_from_re))?; - let midi_out = connect_midi_outs(&jack, &"M".to_string(), &self.midi_to, Some(&self.midi_to_re))?; - let inputs = [].into_iter() - .chain(connect_audio_ins(&jack, &"L".to_string(), &self.left_from, None)?.into_iter()) - .chain(connect_audio_ins(&jack, &"R".to_string(), &self.right_from, None)?.into_iter()); - let outputs = [].into_iter() - .chain(connect_audio_outs(&jack, &"L".to_string(), &self.left_to, None)?.into_iter()) - .chain(connect_audio_outs(&jack, &"R".to_string(), &self.right_to, None)?.into_iter()); - let mut proj = Project::create(&jack, name.into(), Clock::new(&jack, self.bpm)?, - [].into_iter(), [].into_iter(), midi_ins.into_iter(), midi_out.into_iter(), - inputs, outputs); - proj.tracks_add_many(self.tracks.unwrap_or(0), None, [].into(), [].into())?; - proj.scenes_add_many(self.scenes.unwrap_or(0))?; - Ok(proj) - } - } -} - -/// Arranger. -/// -/// ``` -/// let arranger = tek::Project::default(); -/// ``` -#[namespace(ClipPool ClipPool::eval)] -#[derive(Default, Debug)] -pub struct Project { - /// JACK client handle. - pub jack: Jack<'static>, - /// Project name. - pub name: Arc, - /// Base color. - pub color: ItemTheme, - /// FIXME a render of the project arrangement, redrawn on update. - /// TODO rename to "render_cache" or smth - pub arranger: Arc>, - /// Display size - pub size: Sizer, - /// Display size of clips area - pub size_inner: Sizer, - /// Selected UI element - pub selection: Selection, - /// Contains all recently created clips. - #[cfg(feature = "sequencer")] pub clips: Pool, - /// Contains all recently created clips. - #[cfg(feature = "sampler")] pub samples: Pool, - /// Source of time - #[cfg(feature = "clock")] pub clock: Clock, - /// Allows one MIDI clip to be edited - #[cfg(feature = "editor")] pub editor: Option, - /// List of global midi inputs - #[cfg(feature = "port")] pub midi_ins: Vec, - /// List of global midi outputs - #[cfg(feature = "port")] pub midi_outs: Vec, - /// List of global audio inputs - #[cfg(feature = "port")] pub audio_ins: Vec, - /// List of global audio outputs - #[cfg(feature = "port")] pub audio_outs: Vec, - /// Last track number (to avoid duplicate port names) - #[cfg(feature = "track")] pub track_last: usize, - /// List of tracks - #[cfg(feature = "track")] pub tracks: Vec, - /// Scroll offset of tracks - #[cfg(feature = "track")] pub track_scroll: usize, - /// List of scenes - #[cfg(feature = "scene")] pub scenes: Vec, - /// Scroll offset of scenes - #[cfg(feature = "scene")] pub scenes_scroll: usize, -} - -impl Project { - pub fn new (jack: &Jack<'static>) -> Self { - Self { - jack: jack.clone(), - ..Default::default() - } - } - - pub fn new_jack (name: impl AsRef) -> Usually { - let name = name.as_ref(); - Ok(Self { - name: name.into(), - jack: Jack::new(name)?, - ..Default::default() - }) - } - - /// Create a new arrangement. - pub fn create ( - jack: &Jack<'static>, - name: Arc, - clock: Clock, - tracks: impl Iterator, - scenes: impl Iterator, - midi_ins: impl Iterator, - midi_outs: impl Iterator, - audio_ins: impl Iterator, - audio_outs: impl Iterator, - ) -> Self { - Self { - jack: jack.clone(), - color: ItemTheme::random(), - selection: Selection::TrackClip { track: 0, scene: 0 }, - tracks: tracks.collect(), - scenes: scenes.collect(), - midi_ins: midi_ins.collect(), - midi_outs: midi_outs.collect(), - audio_ins: audio_ins.collect(), - audio_outs: audio_outs.collect(), - size_inner: Sizer(Arc::new(40.into()), Arc::new(25.into())), - clock, - name, - ..Default::default() - } - } - - /// Width of display - pub fn w (&self) -> u16 { - self.size.w() as u16 - } - - /// Width allocated for sidebar. - pub fn w_sidebar (&self, is_editing: bool) -> u16 { - self.w() / if is_editing { 16 } else { 8 } as u16 - } - - /// Width available to display tracks. - pub fn w_tracks_area (&self, is_editing: bool) -> u16 { - self.w().saturating_sub(self.w_sidebar(is_editing)) - } - - /// Height of display - pub fn h (&self) -> u16 { - self.size.h() as u16 - } - - /// Height taken by visible device slots. - pub fn h_devices (&self) -> u16 { - 2 - //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) - } -} - -impl HasJack<'static> for Project { - fn jack (&self) -> &Jack<'static> { - &self.jack - } -} - -impl HasClip for Project { - fn clip_slot (&self) -> Option> { - match self.selection { - Selection::Clip(index) => - Some(self.clips.slot(index)), - Selection::TrackClip { track, scene } if let Some(scene) = self.scenes.get(scene) => - Some(scene.clips.slot(track)), - _ => - None - } - } -} - -impl HasClipsSize for Project { - fn clips_size (&self) -> &Sizer { - &self.size_inner - } -} - -impl ScenesView for Project { - fn h_scenes (&self) -> u16 { - (self.size.h() as u16).saturating_sub(20) - } - fn w_side (&self) -> u16 { - (self.size.w() as u16 * 2 / 10).max(20) - } - fn w_mid (&self) -> u16 { - (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) - } -} - -pub fn swap_value ( - target: &mut T, value: &T, returned: impl Fn(T)->U -) -> Perhaps { - if *target == *value { - Ok(None) - } else { - let mut value = value.clone(); - std::mem::swap(target, &mut value); - Ok(Some(returned(value))) - } -} - -pub fn toggle_bool ( - target: &mut bool, value: &Option, returned: impl Fn(Option)->U -) -> Perhaps { - let mut value = value.unwrap_or(!*target); - if value == *target { - Ok(None) - } else { - std::mem::swap(target, &mut value); - Ok(Some(returned(Some(value)))) - } -} - -/// Modes for clip pool -#[derive(Debug, Clone)] pub enum PoolMode { - /// Renaming a pattern - Rename(usize, Arc), - /// Editing the length of a pattern - Length(usize, usize, ClipLengthFocus), - /// Load clip from disk - Import(usize, Browse), - /// Save clip to disk - Export(usize, Browse), -} - -/// Focused field of `ClipLength` -#[derive(Copy, Clone, Debug)] pub enum ClipLengthFocus { - /// Editing the number of bars - Bar, - /// Editing the number of beats - Beat, - /// Editing the number of ticks - Tick, -} - -impl ClipLengthFocus { - pub fn next (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } - } - pub fn prev (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } - } -} - -/// Displays and edits clip length. -#[derive(Clone, Debug, Default)] pub struct ClipLength { - /// Pulses per beat (quaver) - pub ppq: usize, - /// Beats per bar - pub bpb: usize, - /// Length of clip in pulses - pub pulses: usize, - /// Selected subdivision - pub focus: Option, -} - -impl ClipLength { - pub fn _new (pulses: usize, focus: Option) -> Self { - Self { ppq: PPQ, bpb: 4, pulses, focus } - } - pub fn bars (&self) -> usize { - self.pulses / (self.bpb * self.ppq) - } - pub fn beats (&self) -> usize { - (self.pulses % (self.bpb * self.ppq)) / self.ppq - } - pub fn ticks (&self) -> usize { - self.pulses % self.ppq - } -} - -impl HasTrackScroll for &mut Project { - fn track_scroll (&self) -> usize { - self.track_scroll - } - fn tracks_last (&self) -> usize { - self.track_last - } - fn tracks_last_mut (&mut self) -> &mut usize { - &mut self.track_last - } -} - -impl HasTrackScroll for Project { - fn track_scroll (&self) -> usize { - self.track_scroll - } - fn tracks_last (&self) -> usize { - self.track_last - } - fn tracks_last_mut (&mut self) -> &mut usize { - &mut self.track_last - } -} - -impl HasScenes for Project { - fn scenes_scroll (&self) -> usize { - self.scenes_scroll - } - fn scenes (&self) -> &Vec { - &self.scenes - } - fn scenes_mut (&mut self) -> &mut Vec { - &mut self.scenes - } -} - -pub struct Junction(T); - -impl Draw for Junction { - fn draw (&self, to: &mut Tui) -> Drawn { - T::KIND.draw(to) - } -} - -/// Represents the current user selection in the arranger -#[derive(PartialEq, Clone, Copy, Debug, Default)] -pub enum Selection { - #[default] - /// Nothing is selected - Nothing, - /// The whole mix is selected - Mix, - /// A MIDI input is selected. - Input(usize), - /// A MIDI output is selected. - Output(usize), - /// A clip is selected. - Clip(usize), - /// A sample is selected. - Sample(usize), - /// A scene is selected. - #[cfg(feature = "scene")] Scene(usize), - /// A track is selected. - #[cfg(feature = "track")] Track(usize), - /// A clip slot (track × scene) is selected. - #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, - /// A track's MIDI input connection is selected. - #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, - /// A track's MIDI output connection is selected. - #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, - /// A track device slot is selected. - #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, -} - -impl Selection { - - pub fn describe ( - &self, - #[cfg(feature = "track")] tracks: &[Track], - #[cfg(feature = "scene")] scenes: &[Scene], - ) -> Arc { - use Selection::*; - format!("{}", match self { - Mix => "Everything".to_string(), - #[cfg(feature = "scene")] Scene(s) => - scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), - #[cfg(feature = "track")] Track(t) => - tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), - TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { - (Some(_), Some(s)) => match &s.clips.get(*track) { - None => format!("T{track} S{scene}: Empty"), - Some(clip) => format!("T{track} S{scene} C{}", - clip.as_ref().map(|clip|clip.read().name.clone()).unwrap_or_default()), - }, - _ => format!("T{track} S{scene}: Empty"), - }, - _ => todo!() - }).into() - } - -} - -#[cfg(feature = "scene")] -impl Selection { - - pub fn scene (&self) -> Option { - use Selection::*; - match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } - } - - pub fn select_scene (&self, scene_count: usize) -> Self { - use Selection::*; - match self { - Mix | Track(_) => Scene(0), - Scene(s) => Scene((s + 1) % scene_count), - TrackClip { scene, .. } => Track(*scene), - _ => todo!(), - } - } - - pub fn select_scene_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Scene(0), - Track(t) => TrackClip { track: *t, scene: 0 }, - Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, - TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, - _ => todo!() - } - } - - pub fn select_scene_prev (&self) -> Self { - use Selection::*; - match self { - Mix | Scene(0) => Mix, - Scene(s) => Scene(s - 1), - Track(t) => Track(*t), - TrackClip { track, scene: 0 } => Track(*track), - TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, - _ => todo!() - } - } - -} - -#[cfg(feature = "track")] -impl Selection { - - pub fn track (&self) -> Option { - use Selection::*; - if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { - Some(*track) - } else { - None - } - } - - pub fn select_track (&self, track_count: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(_) => Mix, - Track(t) => Track((t + 1) % track_count), - TrackClip { track, .. } => Track(*track), - _ => todo!(), - } - } - - pub fn select_track_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(s) => TrackClip { track: 0, scene: *s }, - Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, - TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, - _ => todo!() - } - } - - pub fn select_track_prev (&self) -> Self { - use Selection::*; - match self { - Mix => Mix, - Scene(s) => Scene(*s), - Track(0) => Mix, - Track(t) => Track(t - 1), - TrackClip { track: 0, scene } => Scene(*scene), - TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, - _ => todo!() - } - } - -} - -impl SelectionModel for T {} - -pub trait SelectionModel: HasSelection { - /// Get the active track - #[cfg(feature = "track")] fn selected_track (&self) -> Option<&Track> where Self: HasTracks { - let index = self.selection().track()?; - self.tracks().get(index) - } - /// Get a mutable reference to the active track - #[cfg(feature = "track")] fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { - let index = self.selection().track()?; - self.tracks_mut().get_mut(index) - } - /// Get the active scene - #[cfg(feature = "scene")] fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { - let index = self.selection().scene()?; - self.scenes().get(index) - } - /// Get a mutable reference to the active scene - #[cfg(feature = "scene")] fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { - let index = self.selection().scene()?; - self.scenes_mut().get_mut(index) - } - /// Get the active clip - #[cfg(feature = "clip")] fn selected_clip (&self) -> Option> where Self: HasScenes + HasTracks { - self.selected_scene()?.clips.read().get(self.selection().track()?).cloned() - } -} - -/// A scene consists of a set of clips to play together. -/// -/// ``` -/// let scene: tek::Scene = Default::default(); -/// let _ = scene.pulses(); -/// let _ = scene.is_playing(&[]); -/// ``` -#[derive(Debug, Default)] pub struct Scene { - /// Name of scene - pub name: Arc, - /// Identifying color of scene - pub color: ItemTheme, - /// Clips in scene, one per track - pub clips: Pool, -} - -impl Scene { - pub const DEFAULT_HEIGHT: usize = 2; - /// Get pulse length of the longest clip in the scene - pub fn pulses (&self) -> usize { - self.clips.read().iter().fold(0, |a, p|{ - a.max(p.read().as_ref().map(|q|q.read().length).unwrap_or(0)) - }) - } - /// True if all clips in scene are currently playing on given tracks. - pub fn is_playing (&self, tracks: &[Track]) -> bool { - // Not "playing" if there are no clips - if !self.clips.read().iter().any(|clip|clip.read().is_some()) { - return false - } - // Playing if at least 1 clip is playing - self.clips.read().iter().enumerate().all(|(track_index, clip)|match &*clip.read() { - Some(c) => tracks - .get(track_index) - .map(|track|{ - if let Some((_, Some(clip))) = track.sequencer().play_clip() { - *clip.read() == *c.read() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) - } -} - -pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T); diff --git a/src/tek.rs b/src/tek.rs index 90df98dc..f334ce37 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -1,5 +1,18 @@ #![allow(clippy::unit_arg)] +impl_as_mut!(Option: |self: Project| &mut self.editor); +impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut()); +impl_as_mut_opt!(Track: |self: Project| self.selected_track_mut()); +impl_as_ref!(Option: |self: Project| &self.editor); +impl_as_ref_opt!(Scene: |self: Project| self.selected_scene()); +impl_as_ref_opt!(Track: |self: Project| self.selected_track()); +impl_has!(Clock: |self: Project| self.clock); +impl_has!(Jack<'static>: |self: Project| self.jack); +impl_has!(Selection: |self: Project| self.selection); +impl_has!(Sizer: |self: Project| self.size); +impl_has!(Vec: |self: Project| self.midi_ins); +impl_has!(Vec: |self: Project| self.midi_outs); + impl_as_ref!(Config: |self: App|self.config.config.as_ref()); impl_as_mut!(Option: |self: App|self.project.as_mut()); impl_as_mut!(Option: |self: App|todo!()); @@ -46,14 +59,15 @@ macro_rules! def_sizes_iter { } } -mod conf; pub use self::conf::*; -mod deps; pub use self::deps::*; -mod pool; pub use self::pool::*; -mod project; pub use self::project::*; -mod sequence; pub use self::sequence::*; +def_sizes_iter!(ScenesSizes => Scene); +def_sizes_iter!(TracksSizes => Track); +def_sizes_iter!(PortsSizes => Arc, [Connect]); -pub use self::device::*; -mod device { +pub use self::conf::*; mod conf; +pub use self::deps::*; mod deps; +pub use self::gear::*; mod gear { + mod pool; pub use self::pool::*; + mod midi; pub use self::midi::*; mod mixer; pub use self::mixer::*; mod sample; pub use self::sample::*; @@ -223,13 +237,69 @@ fn main_plain (config: Arc) -> Usually<()> { Ok(()) } } + + /// Command to launch into new project from CLI. + #[derive(Debug, Clone, Parser, Default)] + pub struct ProjectInit { + /// Name of JACK client + #[arg(short='n', long)] name: Option, + /// Whether to attempt to become transport master + #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, + /// Whether to sync to external transport master + #[arg(short='y', long, default_value_t = true)] sync_follow: bool, + /// Initial tempo in beats per minute + #[arg(short='b', long, default_value = None)] bpm: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short='c', long, default_value_t = true)] show_clock: bool, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='I', long)] midi_from: Vec, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='i', long)] midi_from_re: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='O', long)] midi_to: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='o', long)] midi_to_re: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] left_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] right_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] left_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] right_to: Vec, + /// Tracks to creat + #[arg(short='t', long)] tracks: Option, + /// Scenes to create + #[arg(short='s', long)] scenes: Option, + } + + impl ProjectInit { + pub fn init (&self) -> Usually { + let name = self.name.as_ref().map_or("tek", |x|x.as_str()); + let jack = Jack::new(&name)?; + let midi_ins = connect_midi_ins(&jack, &"M".to_string(), &self.midi_from, Some(&self.midi_from_re))?; + let midi_out = connect_midi_outs(&jack, &"M".to_string(), &self.midi_to, Some(&self.midi_to_re))?; + let inputs = [].into_iter() + .chain(connect_audio_ins(&jack, &"L".to_string(), &self.left_from, None)?.into_iter()) + .chain(connect_audio_ins(&jack, &"R".to_string(), &self.right_from, None)?.into_iter()); + let outputs = [].into_iter() + .chain(connect_audio_outs(&jack, &"L".to_string(), &self.left_to, None)?.into_iter()) + .chain(connect_audio_outs(&jack, &"R".to_string(), &self.right_to, None)?.into_iter()); + let mut proj = Project::create(&jack, name.into(), Clock::new(&jack, self.bpm)?, + [].into_iter(), [].into_iter(), midi_ins.into_iter(), midi_out.into_iter(), + inputs, outputs); + proj.tracks_add_many(self.tracks.unwrap_or(0), None, [].into(), [].into())?; + proj.scenes_add_many(self.scenes.unwrap_or(0))?; + Ok(proj) + } + } } /// Total application state. /// /// ``` -/// use tek::{HasTracks, HasScenes, TracksView, ScenesView, ScenesControl}; -/// let mut app = tek::App::default(); +/// # use tek::*; +/// let mut app = App::default(); /// let _ = app.scenes_add_one(None, None).unwrap(); /// let _ = app.update_clock(); /// app.project.editor = Some(Default::default()); @@ -249,34 +319,34 @@ fn main_plain (config: Arc) -> Usually<()> { /// let _ = app.project.h_scenes(); /// ``` #[derive(Default, Debug)] -#[namespace(Arc<[Connect]> get_arc_array_connect)] -#[namespace(Arc get_arc_str)] -#[namespace(Color get_color)] -#[namespace(ControlAxis ControlAxis::get_axis)] -#[namespace(Dialog Dialog::get_dialog)] -#[namespace(ItemColor)] -#[namespace(ItemTheme)] -#[namespace(MidiClip)] +#[namespace(Arc<[Connect]> tek_arc_array_connect)] +#[namespace(Arc tek_arc_str)] +#[namespace(Color tek_color)] +#[namespace(Option> tek_opt_arc_array_connect)] +#[namespace(Option> tek_opt_arc_str)] +#[namespace(Option tek_opt_itemtheme)] +#[namespace(Option tek_opt_bool)] +#[namespace(Option tek_opt_u16)] +#[namespace(Option tek_opt_u32)] +#[namespace(Option tek_opt_usize)] +#[namespace(PathBuf tek_path_buf)] +#[namespace(Vec> tek_vec_arc_str)] +#[namespace(bool tek_bool)] +#[namespace(f64 tek_f64)] +#[namespace(u16 tek_u16)] +#[namespace(usize tek_usize)] +#[namespace(ControlAxis ControlAxis::eval)] +#[namespace(Dialog Dialog::eval)] #[namespace(ClipSlot ClipSlot::eval)] #[namespace(ClipPool ClipPool::eval)] #[namespace(SampleSlot SampleSlot::eval)] #[namespace(SamplePool SamplePool::eval)] -#[namespace(Option> get_opt_arc_array_connect)] -#[namespace(Option> get_opt_arc_str)] -#[namespace(Option get_opt_itemtheme)] -#[namespace(Option get_opt_bool)] -#[namespace(Option get_opt_u16)] -#[namespace(Option get_opt_u32)] -#[namespace(Option get_opt_usize)] -#[namespace(PathBuf get_path_buf)] -#[namespace(Vec> get_vec_arc_str)] -#[namespace(bool get_bool)] -#[namespace(f64 get_f64)] +#[namespace(ItemColor)] +#[namespace(ItemTheme)] +#[namespace(MidiClip)] #[namespace(isize)] -#[namespace(u16 get_u16)] #[namespace(u32)] #[namespace(u8)] -#[namespace(usize get_usize)] pub struct App { /// Exit flag pub exit: Exit, @@ -346,13 +416,6 @@ impl App { } } -impl HasJack<'static> for App { - fn jack (&self) -> &Jack<'static> { - &self.jack - } -} - - fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control { let t0 = state.perf.get_t0(); state.clock().update_from_scope(scope).unwrap(); @@ -400,174 +463,491 @@ fn tek_jack_event (state: &mut App, event: JackEvent) { } } -fn get_arc_str <'a> (_: &App, src: impl Language) -> Perhaps> { - Ok(src.src()?.map(|x|x.into())) +/// Arranger. +/// +/// ``` +/// let arranger = tek::Project::default(); +/// ``` +#[namespace(ClipPool ClipPool::eval)] +#[derive(Default, Debug)] +pub struct Project { + /// JACK client handle. + pub jack: Jack<'static>, + /// Project name. + pub name: Arc, + /// Base color. + pub color: ItemTheme, + /// FIXME a render of the project arrangement, redrawn on update. + /// TODO rename to "render_cache" or smth + pub arranger: Arc>, + /// Display size + pub size: Sizer, + /// Display size of clips area + pub size_inner: Sizer, + /// Selected UI element + pub selection: Selection, + /// Contains all recently created clips. + #[cfg(feature = "sequencer")] pub clips: Pool, + /// Contains all recently created clips. + #[cfg(feature = "sampler")] pub samples: Pool, + /// Source of time + #[cfg(feature = "clock")] pub clock: Clock, + /// Allows one MIDI clip to be edited + #[cfg(feature = "editor")] pub editor: Option, + /// List of global midi inputs + #[cfg(feature = "port")] pub midi_ins: Vec, + /// List of global midi outputs + #[cfg(feature = "port")] pub midi_outs: Vec, + /// List of global audio inputs + #[cfg(feature = "port")] pub audio_ins: Vec, + /// List of global audio outputs + #[cfg(feature = "port")] pub audio_outs: Vec, + /// Last track number (to avoid duplicate port names) + #[cfg(feature = "track")] pub track_last: usize, + /// List of tracks + #[cfg(feature = "track")] pub tracks: Vec, + /// Scroll offset of tracks + #[cfg(feature = "track")] pub track_scroll: usize, + /// List of scenes + #[cfg(feature = "scene")] pub scenes: Vec, + /// Scroll offset of scenes + #[cfg(feature = "scene")] pub scenes_scroll: usize, } -fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps { - Ok(Some(match src.word()? { - Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()), - Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9), - _ => return try_to_u16(src) - })) -} - -fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps { - Ok(Some(match src.word()? { - Some(":scene-count") => state.scenes().len(), - Some(":track-count") => state.tracks().len(), - Some(":device-kind") => state.dialog.device_kind().unwrap_or(0), - Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0), - Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0), - _ => return try_to_usize(src) - })) -} - -fn get_f64 <'a> (_state: &App, _src: impl Language) -> Perhaps { - todo!() -} - -fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps { - src.word()?.map(|word|Ok(match word { - "Y" => true, - "N" => false, - ":mode/editor" => state.project.editor.is_some(), - ":focused/dialog" => !matches!(state.dialog, Dialog::None), - ":focused/message" => matches!(state.dialog, Dialog::Message(..)), - ":focused/add_device" => matches!(state.dialog, Dialog::Device(..)), - ":focused/browser" => state.dialog.browser().is_some(), - //":focused/pool/import" => matches!(state.project.pool.mode, Some(PoolMode::Import(..))), - //":focused/pool/export" => matches!(state.project.pool.mode, Some(PoolMode::Export(..))), - //":focused/pool/rename" => matches!(state.project.pool.mode, Some(PoolMode::Rename(..))), - //":focused/pool/length" => matches!(state.project.pool.mode, Some(PoolMode::Length(..))), - ":focused/clip" => matches!(state.selection(), Selection::TrackClip{..}), - ":focused/track" => matches!(state.selection(), Selection::Track(..)), - ":focused/scene" => matches!(state.selection(), Selection::Scene(..)), - ":focused/mix" => matches!(state.selection(), Selection::Mix), - _ => return err!("not bool: {word}") - })).transpose() -} - -#[allow(unused)] -fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps { - src.word()?.map(|word|Ok(match word { - ":select/scene" => state.selection().select_scene(state.tracks().len()), - ":select/scene/next" => state.selection().select_scene_next(state.scenes().len()), - ":select/scene/prev" => state.selection().select_scene_prev(), - ":select/track" => state.selection().select_track(state.tracks().len()), - ":select/track/next" => state.selection().select_track_next(state.tracks().len()), - ":select/track/prev" => state.selection().select_track_prev(), - _ => return err!("not selection: {word}") - })).transpose() -} - -fn get_color <'a> (_: &App, src: impl Language) -> Perhaps { - if let Some(expr) = src.expr()? { - match (expr.head()?, expr.tail()?) { - (Some("g"), Some(tail)) => { - let n = try_to_u8(tail.head().map_err(Into::into))? - .ok_or(LanguageError::domain("not gray"))?; - Ok(Some(Color::Rgb(n, n, n))) - }, - (Some("rgb"), Some(tail)) => { - let r = try_to_u8(tail.head().map_err(Into::into))? - .ok_or(LanguageError::domain("not red"))?; - let g = try_to_u8(tail.tail().head().map_err(Into::into))? - .ok_or(LanguageError::domain("not green"))?; - let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))? - .ok_or(LanguageError::domain("not blue"))?; - Ok(Some(Color::Rgb(r, g, b))) - }, - (Some(_), _) => return err!("not a color expression: {expr}"), - (None, _) => return err!("not a color expression: {expr}"), +impl Project { + pub fn new (jack: &Jack<'static>) -> Self { + Self { + jack: jack.clone(), + ..Default::default() } - } else if let Ok(Some(sym)) = src.word() { - Ok(match sym { - ":color/bg" => Some(Color::Rgb(28, 32, 36)), - ":color/fg" => Some(Color::Rgb(98, 92, 96)), - _ => return err!("not a color: {sym}") + } + + pub fn new_jack (name: impl AsRef) -> Usually { + let name = name.as_ref(); + Ok(Self { + name: name.into(), + jack: Jack::new(name)?, + ..Default::default() }) - } else { - return err!("not a color: {:?}", src.src()?) + } + + /// Create a new arrangement. + pub fn create ( + jack: &Jack<'static>, + name: Arc, + clock: Clock, + tracks: impl Iterator, + scenes: impl Iterator, + midi_ins: impl Iterator, + midi_outs: impl Iterator, + audio_ins: impl Iterator, + audio_outs: impl Iterator, + ) -> Self { + Self { + jack: jack.clone(), + color: ItemTheme::random(), + selection: Selection::TrackClip { track: 0, scene: 0 }, + tracks: tracks.collect(), + scenes: scenes.collect(), + midi_ins: midi_ins.collect(), + midi_outs: midi_outs.collect(), + audio_ins: audio_ins.collect(), + audio_outs: audio_outs.collect(), + size_inner: Sizer(Arc::new(40.into()), Arc::new(25.into())), + clock, + name, + ..Default::default() + } + } + + /// Width of display + pub fn w (&self) -> u16 { + self.size.w() as u16 + } + + /// Width allocated for sidebar. + pub fn w_sidebar (&self, is_editing: bool) -> u16 { + self.w() / if is_editing { 16 } else { 8 } as u16 + } + + /// Width available to display tracks. + fn w_tracks_area (&self, is_editing: bool) -> u16 { + self.w().saturating_sub(self.w_sidebar(is_editing)) + } + + /// Height of display + pub fn h (&self) -> u16 { + self.size.h() as u16 + } + + /// Height taken by visible device slots. + fn h_devices (&self) -> u16 { + 2 + //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } } -#[allow(unused)] -fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps> { - eval_word(src, |word|Ok(match word { - - ":editor/pitch" => Some(( - state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8 - ).into()), - - _ => return err!("unknown midi note: {word}") - - })) +impl HasJack<'static> for App { + fn jack (&self) -> &Jack<'static> { + &self.jack + } } - -fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps> { - eval_word(src, |word|Ok(match word { - - ":selected/scene" => state.selection().scene(), - ":selected/track" => state.selection().track(), - - _ => return err!("unknown opt: {word}") - - })) +impl HasJack<'static> for Project { + fn jack (&self) -> &Jack<'static> { + &self.jack + } } -fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps> { - Ok(None) +impl ScenesView for Project { + fn h_scenes (&self) -> u16 { + (self.size.h() as u16).saturating_sub(20) + } + fn w_side (&self) -> u16 { + (self.size.w() as u16 * 2 / 10).max(20) + } + fn w_mid (&self) -> u16 { + (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) + } } -fn get_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps> { - Ok(None) +pub fn swap_value ( + target: &mut T, value: &T, returned: impl Fn(T)->U +) -> Perhaps { + if *target == *value { + Ok(None) + } else { + let mut value = value.clone(); + std::mem::swap(target, &mut value); + Ok(Some(returned(value))) + } } -fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps> { - todo!() +pub fn toggle_bool ( + target: &mut bool, value: &Option, returned: impl Fn(Option)->U +) -> Perhaps { + let mut value = value.unwrap_or(!*target); + if value == *target { + Ok(None) + } else { + std::mem::swap(target, &mut value); + Ok(Some(returned(Some(value)))) + } } -fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps>> { - todo!() +/// Modes for clip pool +#[derive(Debug, Clone)] pub enum PoolMode { + /// Renaming a pattern + Rename(usize, Arc), + /// Editing the length of a pattern + Length(usize, usize, ClipLengthFocus), + /// Load clip from disk + Import(usize, Browse), + /// Save clip to disk + Export(usize, Browse), } -fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps> { - todo!() +/// Focused field of `ClipLength` +#[derive(Copy, Clone, Debug)] pub enum ClipLengthFocus { + /// Editing the number of bars + Bar, + /// Editing the number of beats + Beat, + /// Editing the number of ticks + Tick, } -fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps>> { - todo!() +impl ClipLengthFocus { + pub fn next (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } + } + pub fn prev (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } + } } -fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps> { - todo!() +/// Displays and edits clip length. +#[derive(Clone, Debug, Default)] pub struct ClipLength { + /// Pulses per beat (quaver) + pub ppq: usize, + /// Beats per bar + pub bpb: usize, + /// Length of clip in pulses + pub pulses: usize, + /// Selected subdivision + pub focus: Option, } -fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps { - todo!() +impl ClipLength { + pub fn _new (pulses: usize, focus: Option) -> Self { + Self { ppq: PPQ, bpb: 4, pulses, focus } + } + pub fn bars (&self) -> usize { + self.pulses / (self.bpb * self.ppq) + } + pub fn beats (&self) -> usize { + (self.pulses % (self.bpb * self.ppq)) / self.ppq + } + pub fn ticks (&self) -> usize { + self.pulses % self.ppq + } } -fn get_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps>> { - todo!() +impl HasScenes for Project { + fn scenes_scroll (&self) -> usize { + self.scenes_scroll + } + fn scenes (&self) -> &Vec { + &self.scenes + } + fn scenes_mut (&mut self) -> &mut Vec { + &mut self.scenes + } } +pub struct Junction(T); + +impl Draw for Junction { + fn draw (&self, to: &mut Tui) -> Drawn { + T::KIND.draw(to) + } +} + +/// Represents the current user selection in the arranger +#[derive(PartialEq, Clone, Copy, Debug, Default)] +pub enum Selection { + #[default] + /// Nothing is selected + Nothing, + /// The whole mix is selected + Mix, + /// A MIDI input is selected. + Input(usize), + /// A MIDI output is selected. + Output(usize), + /// A clip is selected. + Clip(usize), + /// A sample is selected. + Sample(usize), + /// A scene is selected. + #[cfg(feature = "scene")] Scene(usize), + /// A track is selected. + #[cfg(feature = "track")] Track(usize), + /// A clip slot (track × scene) is selected. + #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, + /// A track's MIDI input connection is selected. + #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, + /// A track's MIDI output connection is selected. + #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, + /// A track device slot is selected. + #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, +} + +impl Selection { + + pub fn describe ( + &self, + #[cfg(feature = "track")] tracks: &[Track], + #[cfg(feature = "scene")] scenes: &[Scene], + ) -> Arc { + use Selection::*; + format!("{}", match self { + Mix => "Everything".to_string(), + #[cfg(feature = "scene")] Scene(s) => + scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), + #[cfg(feature = "track")] Track(t) => + tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), + TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { + (Some(_), Some(s)) => match &s.clips.get(*track) { + None => format!("T{track} S{scene}: Empty"), + Some(clip) => format!("T{track} S{scene} C{}", + clip.as_ref().map(|clip|clip.read().name.clone()).unwrap_or_default()), + }, + _ => format!("T{track} S{scene}: Empty"), + }, + _ => todo!() + }).into() + } + +} + +#[cfg(feature = "scene")] +impl Selection { + + pub fn scene (&self) -> Option { + use Selection::*; + match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } + } + + pub fn select_scene (&self, scene_count: usize) -> Self { + use Selection::*; + match self { + Mix | Track(_) => Scene(0), + Scene(s) => Scene((s + 1) % scene_count), + TrackClip { scene, .. } => Track(*scene), + _ => todo!(), + } + } + + pub fn select_scene_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Scene(0), + Track(t) => TrackClip { track: *t, scene: 0 }, + Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, + TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, + _ => todo!() + } + } + + pub fn select_scene_prev (&self) -> Self { + use Selection::*; + match self { + Mix | Scene(0) => Mix, + Scene(s) => Scene(s - 1), + Track(t) => Track(*t), + TrackClip { track, scene: 0 } => Track(*track), + TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, + _ => todo!() + } + } + +} + +#[cfg(feature = "track")] +impl Selection { + + pub fn track (&self) -> Option { + use Selection::*; + if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { + Some(*track) + } else { + None + } + } + + pub fn select_track (&self, track_count: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(_) => Mix, + Track(t) => Track((t + 1) % track_count), + TrackClip { track, .. } => Track(*track), + _ => todo!(), + } + } + + pub fn select_track_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(s) => TrackClip { track: 0, scene: *s }, + Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, + TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, + _ => todo!() + } + } + + pub fn select_track_prev (&self) -> Self { + use Selection::*; + match self { + Mix => Mix, + Scene(s) => Scene(*s), + Track(0) => Mix, + Track(t) => Track(t - 1), + TrackClip { track: 0, scene } => Scene(*scene), + TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, + _ => todo!() + } + } + +} + +impl SelectionModel for T {} + +pub trait SelectionModel: HasSelection { + /// Get the active track + #[cfg(feature = "track")] fn selected_track (&self) -> Option<&Track> where Self: HasTracks { + let index = self.selection().track()?; + self.tracks().get(index) + } + /// Get a mutable reference to the active track + #[cfg(feature = "track")] fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { + let index = self.selection().track()?; + self.tracks_mut().get_mut(index) + } + /// Get the active scene + #[cfg(feature = "scene")] fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { + let index = self.selection().scene()?; + self.scenes().get(index) + } + /// Get a mutable reference to the active scene + #[cfg(feature = "scene")] fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { + let index = self.selection().scene()?; + self.scenes_mut().get_mut(index) + } + /// Get the active clip + #[cfg(feature = "clip")] fn selected_clip (&self) -> Option> where Self: HasScenes + HasTracks { + self.selected_scene()?.clips.read().get(self.selection().track()?).cloned() + } +} + +/// A scene consists of a set of clips to play together. +/// +/// ``` +/// let scene: tek::Scene = Default::default(); +/// let _ = scene.pulses(); +/// let _ = scene.is_playing(&[]); +/// ``` +#[derive(Debug, Default)] pub struct Scene { + /// Name of scene + pub name: Arc, + /// Identifying color of scene + pub color: ItemTheme, + /// Clips in scene, one per track + pub clips: Pool, +} + +impl Scene { + pub const DEFAULT_HEIGHT: usize = 2; + /// Get pulse length of the longest clip in the scene + pub fn pulses (&self) -> usize { + self.clips.read().iter().fold(0, |a, p|{ + a.max(p.read().as_ref().map(|q|q.read().length).unwrap_or(0)) + }) + } + /// True if all clips in scene are currently playing on given tracks. + pub fn is_playing (&self, tracks: &[Track]) -> bool { + // Not "playing" if there are no clips + if !self.clips.read().iter().any(|clip|clip.read().is_some()) { + return false + } + // Playing if at least 1 clip is playing + self.clips.read().iter().enumerate().all(|(track_index, clip)|match &*clip.read() { + Some(c) => tracks + .get(track_index) + .map(|track|{ + if let Some((_, Some(clip))) = track.sequencer().play_clip() { + *clip.read() == *c.read() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) + } +} + +pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T); + impl HasModeStack for App { fn mode_stack (&self) -> &RwLock>> { &self.modes } } -impl HasClip for App { - fn clip_slot (&self) -> Option> { - self.project.clip_slot() - } -} -impl HasClipsSize for App { - fn clips_size (&self) -> &Sizer { - &self.project.size_inner - } -} impl ScenesView for App { fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(self.w_side()) @@ -590,17 +970,6 @@ impl HasScenes for App { self.project.scenes_mut() } } -impl HasTrackScroll for App { - fn track_scroll (&self) -> usize { - self.project.track_scroll() - } - fn tracks_last (&self) -> usize { - self.project.track_last - } - fn tracks_last_mut (&mut self) -> &mut usize { - &mut self.project.track_last - } -} /// Various possible dialog modes. /// @@ -619,7 +988,7 @@ impl HasTrackScroll for App { impl Dialog { - fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps { + fn eval <'a> (_: &App, src: impl Language) -> Perhaps { eval_word(&src, |word|Ok(match word { @@ -1184,6 +1553,39 @@ pub trait HasTrackScroll { fn tracks_last (&self) -> usize; fn tracks_last_mut (&mut self) -> &mut usize; } +impl HasTrackScroll for &mut Project { + fn track_scroll (&self) -> usize { + self.track_scroll + } + fn tracks_last (&self) -> usize { + self.track_last + } + fn tracks_last_mut (&mut self) -> &mut usize { + &mut self.track_last + } +} +impl HasTrackScroll for Project { + fn track_scroll (&self) -> usize { + self.track_scroll + } + fn tracks_last (&self) -> usize { + self.track_last + } + fn tracks_last_mut (&mut self) -> &mut usize { + &mut self.track_last + } +} +impl HasTrackScroll for App { + fn track_scroll (&self) -> usize { + self.project.track_scroll() + } + fn tracks_last (&self) -> usize { + self.project.track_last + } + fn tracks_last_mut (&mut self) -> &mut usize { + &mut self.project.track_last + } +} pub trait HasSampler: AsRef + AsMut { fn sampler (&self) -> &Sampler { @@ -1203,6 +1605,23 @@ pub trait HasClip { self.clip_slot().map(|slot|slot.write()) } } +impl HasClip for App { + fn clip_slot (&self) -> Option> { + self.project.clip_slot() + } +} +impl HasClip for Project { + fn clip_slot (&self) -> Option> { + match self.selection { + Selection::Clip(index) => + Some(self.clips.slot(index)), + Selection::TrackClip { track, scene } if let Some(scene) = self.scenes.get(scene) => + Some(scene.clips.slot(track)), + _ => + None + } + } +} pub trait HasWidth { const MIN_WIDTH: usize; @@ -1224,6 +1643,47 @@ pub trait HasClipsSize { ) } } +impl HasClipsSize for App { + fn clips_size (&self) -> &Sizer { + &self.project.size_inner + } +} +impl HasClipsSize for Project { + fn clips_size (&self) -> &Sizer { + &self.size_inner + } +} + + +pub fn print_status (project: &Project) { + println!("Name: {:?}", &project.name); + println!("JACK: {:?}", &project.jack); + println!("Buffer: {:?}", &project.clock.chunk); + println!("Sample rate: {:?}", &project.clock.timebase.sr); + println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); + println!("Tempo: {:?}", &project.clock.timebase.bpm); + println!("Quantize: {:?}", &project.clock.quant); + println!("Launch: {:?}", &project.clock.sync); + println!("Playhead: {:?}us", &project.clock.playhead.usec); + println!("Playhead: {:?}s", &project.clock.playhead.sample); + println!("Playhead: {:?}p", &project.clock.playhead.pulse); + println!("Started: {:?}", &project.clock.started); + println!("Tracks:"); + for (i, t) in project.tracks.iter().enumerate() { + println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, + &t.sequencer.play_clip, &t.sequencer.next_clip); + } + println!("Scenes:"); + for (i, t) in project.scenes.iter().enumerate() { + println!(" Scene {i}: {} {:?}", &t.name, &t.clips); + } + println!("MIDI Ins: {:?}", &project.midi_ins); + println!("MIDI Outs: {:?}", &project.midi_outs); + println!("Audio Ins: {:?}", &project.audio_ins); + println!("Audio Outs: {:?}", &project.audio_outs); + // TODO git integration + // TODO dawvert integration +} tui_keys!(self: App, input { #[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!"); @@ -2022,33 +2482,3 @@ pub enum MidiOutputCmd { Close, Connect(Arc), } - -pub fn print_status (project: &Project) { - println!("Name: {:?}", &project.name); - println!("JACK: {:?}", &project.jack); - println!("Buffer: {:?}", &project.clock.chunk); - println!("Sample rate: {:?}", &project.clock.timebase.sr); - println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); - println!("Tempo: {:?}", &project.clock.timebase.bpm); - println!("Quantize: {:?}", &project.clock.quant); - println!("Launch: {:?}", &project.clock.sync); - println!("Playhead: {:?}us", &project.clock.playhead.usec); - println!("Playhead: {:?}s", &project.clock.playhead.sample); - println!("Playhead: {:?}p", &project.clock.playhead.pulse); - println!("Started: {:?}", &project.clock.started); - println!("Tracks:"); - for (i, t) in project.tracks.iter().enumerate() { - println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, - &t.sequencer.play_clip, &t.sequencer.next_clip); - } - println!("Scenes:"); - for (i, t) in project.scenes.iter().enumerate() { - println!(" Scene {i}: {} {:?}", &t.name, &t.clips); - } - println!("MIDI Ins: {:?}", &project.midi_ins); - println!("MIDI Outs: {:?}", &project.midi_outs); - println!("Audio Ins: {:?}", &project.audio_ins); - println!("Audio Outs: {:?}", &project.audio_outs); - // TODO git integration - // TODO dawvert integration -}