#![allow(clippy::unit_arg)] pub use self::deps::*; mod deps; /// Command-line entrypoint. #[allow(unused)] #[hotpath::main] fn main () -> Usually<()> { tengri::Tui::setup_panic(); #[cfg(not(feature = "cli"))] let outcome = Watch::run_watched(main_plain, None); #[cfg(feature = "cli")] let outcome = Watch::run_watched(main_cli, None); if let Err(e) = outcome { println!("{e:#?}"); std::process::exit(1); } Ok(()) } /// Start with empty project. #[cfg(not(feature = "cli"))] fn main_plain (config: Arc) -> Usually<()> { let name = "tek"; tengri::Tui::run_main(Jack::new_run(name, move|jack|{ let mode = "Menu"; let title = "untitled!"; let bpm = 74.; let clock = Clock::new(&jack, Some(bpm))?; let tracks = []; let scenes = []; let project = Project::create( &jack, title.into(), clock, tracks.into_iter(), scenes.into_iter(), connect_midi_ins(&jack, &"M", &[], None)?.into_iter(), connect_midi_outs(&jack, &"M", &[], None)?.into_iter(), [].into_iter().chain(connect_audio_ins(&jack, &"L", &[], None)?.into_iter()) .chain(connect_audio_ins(&jack, &"R", &[], None)?.into_iter()), [].into_iter().chain(connect_audio_outs(&jack, &"L", &[], None)?.into_iter()) .chain(connect_audio_outs(&jack, &"R", &[], None)?.into_iter()), ); let mut app = App::new(None, project, config, mode); app.dialog = Dialog::welcome(); Ok(app) })?) } /// Total application state. /// /// ``` /// # 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()); /// //let _: Vec<_> = app.project.inputs_with_sizes().collect(); /// //let _: Vec<_> = app.project.outputs_with_sizes().collect(); /// let _: Vec<_> = app.project.tracks_with_sizes().collect(); /// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); /// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); /// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); /// let _ = app.project.w(); /// //let _ = app.project.w_sidebar(); /// //let _ = app.project.w_tracks_area(); /// let _ = app.project.h(); /// //let _ = app.project.h_tracks_area(); /// //let _ = app.project.h_inputs(); /// //let _ = app.project.h_outputs(); /// let _ = app.project.h_scenes(); /// ``` #[derive(Default, Debug)] #[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(ItemColor)] #[namespace(ItemTheme)] #[namespace(MidiClip)] #[namespace(isize)] #[namespace(u32)] #[namespace(u8)] pub struct App { /// Exit flag pub exit: Exit, /// Base color. pub color: ItemTheme, /// Must not be dropped for the duration of the process pub jack: Jack<'static>, /// Display size pub size: Sizer, /// Performance counter pub perf: PerfModel, /// Available view modes and input bindings pub watch: Arc, /// Currently active modes pub stack: Arc>>>, /// Undo history pub history: Vec<(AppCmd, Option)>, /// Dialog overlay pub dialog: Dialog, /// Contains the currently edited musical arrangement pub project: Project, /// Error, if any pub error: Arc>>>, /// Tracing guard #[cfg(feature = "prof2")] pub guard: Option>>, } impl App { /// Create a new application instance from a backend, project, config, and mode /// /// ``` /// # use ::tek::{*, tengri::Jack}; /// let proj = Project::new_jack(&"test_tek").expect("failed to connect to jack"); /// let tek = config_reload(App::new(None, proj, None, "Arranger"), stringify!( /// (mode Arranger (name Arranger) (info Launch grid.) /// (axis X Track) /// (axis Y Scene) /// (bind Global Clock Tracks Scenes Clip Launch Color History) /// (view (bar/n 2 StatusBar (bar/s 8 (bar/e 16 (align/nw Transport) /// (bar/w 20 (bar/s 2 (bar/w 4 TracksOutputsAdd TracksOutputsCount) /// (bar/s 2 (bar/w 4 TracksDevicesAdd TracksDevicesCount) /// (bar/s 2 (bar/w 4 TracksNamesAdd TracksNamesCount) /// (bar/w 4 TracksInputsAdd TracksInputsCount)))) /// (align/x (bar/s 2 TracksOutputsCells /// (bar/s 2 TracksDevicesCells /// (bar/s 2 TracksNamesCells TracksInputsCells)))))) /// (bar/w 20 (text) (bar/e 16 ScenesNames ScenesClips)))))) /// )).unwrap(); /// ``` pub fn new ( exit: Option, project: Project, watch: impl Into>>, mode: impl AsRef ) -> Self { App { exit: exit.unwrap_or_default(), jack: project.jack.clone(), color: ItemTheme::random(), dialog: Dialog::welcome(), stack: Arc::new(RwLock::new(vec![mode.as_ref().into()])), watch: watch.into().unwrap_or_default(), project, ..Default::default() } } } /// Configuration watcher and manager. /// /// ``` /// let config = tek::Watch::new(None); /// ``` #[derive(Default, Debug)] pub struct Watch { /// XDG base directories of running user. pub dirs: BaseDirectories, /// Watcher pub watch: RwLock>, /// App configuration pub config: Config, } impl Watch { /// Default configuration directory. const DIR: &'static str = "tek"; /// Default configuration subdirectory. const SUB: &'static str = "v0"; /// Default configuration file name. const EDN: &'static str = "tek.edn"; /// Create a new watched app configuration /// from a set of XDG base directories, pub fn new (dirs: Option) -> Self { Self { dirs: dirs.unwrap_or_else(||BaseDirectories::with_profile(Self::DIR, Self::SUB)), ..Default::default() } } /// Create, initialize, and watch a new configuration. pub fn run_watched ( callback: impl FnOnce(Arc)->T, dirs: impl Into>, ) -> Usually { let watch = Arc::new(Self::new(dirs.into())); Self::watch(watch.clone(), None)?; let result = callback(watch); Ok(result) } /// Watch a config's file for changes. pub fn watch (config: Arc, poll: Option) -> Usually<()> { let poll = poll.unwrap_or(Duration::from_millis(250)); let opts = NotifyConfig::default().with_poll_interval(poll); let mut watcher = ::notify_debouncer_full::notify::poll::PollWatcher::new({ let config = config.clone(); move|result|{ match result { Ok(_events) => if let Err(e) = config_init(config.as_ref()) { config.error_set(Some(format!("{e:?}").into())); panic!("{e:?}"); } else { //println!("config updated"); }, Err(errors) => { panic!("{errors:?}"); } } } }, opts)?; if let Some(path) = config.get_file() { //println!("watching: {path:?}"); watcher.watch(&path, RecursiveMode::NonRecursive)?; *config.as_ref().watch.try_write().unwrap() = Some(watcher); Ok(()) } else { Err(format!("no config path").into()) } } /// Find the config file. fn find_file (&self) -> Option { self.dirs.find_config_file(Self::EDN) } /// Place config file in default location. fn place_file (&self) -> Result { self.dirs.place_config_file(Self::EDN) } /// Get path to config file. fn get_file (&self) -> Option { self.dirs.get_config_file(Self::EDN) } } /// 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 tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control { let t0 = state.perf.get_t0(); state.clock().update_from_scope(scope).unwrap(); let midi_in = state.project.midi_input_collect(scope); if let Some(editor) = &state.editor() { let mut pitch: Option = None; for port in midi_in.iter() { for event in port.iter() { if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) = event { pitch = Some(key.clone()); } } } if let Some(pitch) = pitch { editor.set_note_pos(pitch.as_int() as usize); } } let result = state.project.tracks_jack_process(client, scope); state.perf.update_from_jack_scope(t0, scope); result } fn tek_jack_event (state: &mut App, event: JackEvent) { use JackEvent::*; match event { SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); }, PortRegistration(_id, true) => { //let port = self.jack().port_by_id(id); //println!("\rport add: {id} {port:?}"); //println!("\rport add: {id}"); }, PortRegistration(_id, false) => { /*println!("\rport del: {id}")*/ }, PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, ClientRegistration(_id, true) => {}, ClientRegistration(_id, false) => {}, ThreadInit => {}, XRun => {}, GraphReorder => {}, _ => { panic!("{event:?}"); } } } 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. 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) } } impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } impl HasJack<'static> for Project { fn jack (&self) -> &Jack<'static> { &self.jack } } 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) } } 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 } } /// 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 }) } } impl ScenesView for App { fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(self.w_side()) } fn w_side (&self) -> u16 { 20 } fn h_scenes (&self) -> u16 { (self.size.h() as u16).saturating_sub(20) } } impl HasScenes for App { fn scenes_scroll (&self) -> usize { self.project.scenes_scroll() } fn scenes (&self) -> &Vec { self.project.scenes() } fn scenes_mut (&mut self) -> &mut Vec { self.project.scenes_mut() } } impl App { /// Return reference to content browser if open. /// /// ``` /// assert_eq!(tek::App::default().browser(), None); /// ``` pub fn browser (&self) -> Option<&Browse> { if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } } } /// Has a MIDI editor. /// /// ``` /// use tek::{*, tengri::*}; /// /// struct Test(Option); /// impl_as_ref!(Option: |self: Test|&self.0); /// impl_as_mut!(Option: |self: Test|&mut self.0); /// /// let mut host = Test(Some(MidiEditor::default())); /// let _ = host.editor(); /// let _ = host.editor_mut(); /// let _ = host.is_editing(); /// let _ = host.editor_w(); /// let _ = host.editor_h(); /// ``` impl HasEditor for T where T: AsRef> + AsMut> {} pub trait HasEditor: AsRef> + AsMut> { fn editor (&self) -> &Option { self.as_ref() } fn editor_mut (&mut self) -> &mut Option { self.as_mut() } } /// Has a pair of buffers for MIDI IO. pub trait HasMidiBuffers { fn note_buf_mut (&mut self) -> &mut Vec; fn midi_buf_mut (&mut self) -> &mut Vec>>; } /// Can have a currently playing clip. pub trait HasPlayClip: HasClock { fn reset (&self) -> bool; fn reset_mut (&mut self) -> &mut bool; fn play_clip (&self) -> &Option<(Moment, Option>>)>; fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; fn next_clip (&self) -> &Option<(Moment, Option>>)>; fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; } /// Has a MIDI sequencer. impl HasSequencer for T where T: AsRef + AsMut {} pub trait HasSequencer: AsRef + AsMut { fn sequencer (&self) -> &Sequencer { self.as_ref() } fn sequencer_mut (&mut self) -> &mut Sequencer { self.as_mut() } } /// Has a clock. pub trait HasClock: AsRef + AsMut { fn clock (&self) -> &Clock { self.as_ref() } fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } } impl HasClock for T where T: AsRef + AsMut {} /// Has a chain of devices. pub trait HasDevices: AsRef> + AsMut> { fn devices (&self) -> &Vec { self.as_ref() } fn devices_mut (&mut self) -> &mut Vec { self.as_mut() } } pub trait HasProject: AsRef> + AsMut> {} /// Has a loaded project. impl HasProject for T where T: AsRef> + AsMut> {} pub trait HasScene: AsRefOpt + AsMutOpt { fn scene (&self) -> Option<&Scene> { self.as_ref_opt() } fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() } } impl HasScene for T where T: AsRefOpt + AsMutOpt + Send + Sync {} pub trait HasScenes { fn scenes (&self) -> &Vec; fn scenes_mut (&mut self) -> &mut Vec; fn scenes_scroll (&self) -> usize { 0 } } impl HasSelection for T where T: AsRef + AsMut {} pub trait HasSelection: AsRef + AsMut { fn selection (&self) -> &Selection { self.as_ref() } fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } } impl HasTrack for T where T: AsRefOpt + AsMutOpt {} pub trait HasTrack: AsRefOpt + AsMutOpt { fn track (&self) -> Option<&Track> { self.as_ref_opt() } fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() } } impl HasTracks for T where T: AsRef> + AsMut> + HasClock + HasTrackScroll {} pub trait HasTracks: AsRef> + AsMut> + HasClock + HasTrackScroll { /// Read-only reference to collection of [Track]s. fn tracks (&self) -> &Vec { self.as_ref() } /// Mutable reference to collection of [Track]s. fn tracks_mut (&mut self) -> &mut Vec { self.as_mut() } } pub trait HasTrackScroll { fn track_scroll (&self) -> usize; 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 HasClip { fn clip_slot (&self) -> Option>; fn clip (&self) -> Option>>>> { self.clip_slot().map(|slot|slot.read()) } fn clip_mut (&self) -> Option>>>> { 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; /// Increment track width. fn width_inc (&mut self); /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. fn width_dec (&mut self); } pub trait HasClipsSize { /// Size of clips area. fn clips_size (&self) -> &Sizer; /// Draw content annotated with value of [Self::clips_size]. fn with_clips_size (&self, show: bool, content: impl Draw) -> impl Draw { let size = self.clips_size(); above( when(show, fg(Green, east!(size.w() as usize, "x", size.h() as usize)).align_se()), size.of(content.align_c().full_wh()) ) } } 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 struct Junction(T); impl Draw for Junction { fn draw (&self, to: &mut Tui) -> Drawn { T::KIND.draw(to) } } /// An item of a menu. /// /// ``` /// let item: tek::MenuItem = Default::default(); /// ``` #[derive(Clone)] pub struct MenuItem(pub Arc, pub Arc<[AppCmd]>); impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } impl MenuItem { pub fn new (name: impl AsRef, actions: [AppCmd; N]) -> Self { Self(name.as_ref().into(), Arc::new(actions)) } } /// Various possible dialog modes. /// /// ``` /// let dialog: tek::Dialog = Default::default(); /// ``` #[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { #[default] None, Help(usize), Menu(usize, Arc<[MenuItem]>), Device(usize), Message(Arc), Browse(BrowseTarget, Arc), Options, } impl Dialog { /// ``` /// let _ = tek::Dialog::welcome(); /// ``` pub fn welcome () -> Self { Self::Menu(1, Arc::new([ MenuItem::new(" Resume session ", [ AppCmd::Project { command: ProjectCmd::LoadLast } ]), MenuItem::new(" Create session ", [ AppCmd::Dialog { dialog: Dialog::None }, AppCmd::Mode { modes: vec!["Arranger".into()] }, AppCmd::Tracks { command: TracksCmd::Add }, AppCmd::Scenes { command: ScenesCmd::Add }, AppCmd::MidiClip { command: MidiClipCmd::Upsert }, AppCmd::MidiEdit { command: MidiEditCmd::Toggle }, ]), MenuItem::new(" Load session ", [ AppCmd::Dialog { dialog: Dialog::Browse( BrowseTarget::LoadProject, Browse::new(None).unwrap().into() ) } ]), MenuItem::new(" Exit ", [ AppCmd::Exit ]), ])) } /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().menu_selected(); /// ``` pub fn menu_selected (&self) -> Option { if let Self::Menu(selected, _) = self { Some(*selected) } else { None } } /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().menu_next(); /// ``` pub fn menu_next (&self) -> Self { match self { Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()), _ => Self::None } } /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().menu_prev(); /// ``` pub fn menu_prev (&self) -> Self { match self { Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()), _ => Self::None } } /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().device_kind(); /// ``` pub fn device_kind (&self) -> Option { if let Self::Device(index) = self { Some(*index) } else { None } } /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().device_kind_next(); /// ``` pub fn device_kind_next (&self) -> Option { self.device_kind().map(|index|(index + 1) % device_kinds().len()) } /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().device_kind_prev(); /// ``` pub fn device_kind_prev (&self) -> Option { self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) } /// FIXME: implement pub fn message (&self) -> Option<&str> { todo!() } /// FIXME: implement pub fn browser (&self) -> Option<&Arc> { todo!() } /// FIXME: implement pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } } /// Browses for files to load/save. /// /// ``` /// let browse = tek::Browse::default(); /// ``` #[derive(Debug, Clone, Default, PartialEq)] pub struct Browse { pub cwd: PathBuf, pub dirs: Vec<(OsString, String)>, pub files: Vec<(OsString, String)>, pub filter: String, pub index: usize, pub scroll: usize, pub size: Sizer, } #[derive(Clone, Debug)] pub enum BrowseTarget { SaveProject, LoadProject, ImportSample(Arc>>), ExportSample(Arc>>), ImportClip(Arc>>), ExportClip(Arc>>), } impl Browse { pub fn new (cwd: Option) -> Usually { let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; let mut dirs = vec![]; let mut files = vec![]; for entry in std::fs::read_dir(&cwd)? { let entry = entry?; let name = entry.file_name(); let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); let meta = entry.metadata()?; if meta.is_dir() { dirs.push((name, format!("📁 {decoded}"))); } else if meta.is_file() { files.push((name, format!("📄 {decoded}"))); } } Ok(Self { cwd, dirs, files, ..Default::default() }) } pub fn to_chdir (&self) -> Usually { Self::new(Some(self.path())) } pub fn len (&self) -> usize { self.dirs.len() + self.files.len() } pub fn is_dir (&self) -> bool { self.index < self.dirs.len() } pub fn is_file (&self) -> bool { self.index >= self.dirs.len() } pub fn path (&self) -> PathBuf { self.cwd.join(if self.is_dir() { &self.dirs[self.index].0 } else if self.is_file() { &self.files[self.index - self.dirs.len()].0 } else { unreachable!() }) } //fn tui_entries (&self) -> EntriesIterator<'_, Tui> { //EntriesIterator { //offset: 0, //index: 0, //length: self.dirs.len() + self.files.len(), //browser: self, //_screen: Default::default(), //} //} } impl PartialEq for BrowseTarget { fn eq (&self, other: &Self) -> bool { match self { Self::ImportSample(_) => false, Self::ExportSample(_) => false, Self::ImportClip(_) => false, Self::ExportClip(_) => false, #[allow(unused)] t => matches!(other, t) } } } pub 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)) } pub trait DispatchBind: HasBinds + HasModeStack + HasModes + Namespace { fn dispatch_bind (&mut self, input: &TuiEvent) -> Usually<()> { if let Some(bindings) = self.dispatch_bind_match(input) { return self.dispatch_bind_exec(bindings) } Ok(()) } /// Find command bindings that match the ,nput. /// ``` /// # use ::{std::sync::Arc, tek::*}; /// // Configure modes: /// let app = config_reload(App::default(), stringify!( /// (axis Y @up @down) /// (bind Confirm (@enter confirm)) /// (mode Test (axis Y Index) (bind (@escape Cancel) Confirm)) /// )).unwrap(); /// println!("Modes: {:#?}", app.watch.config.modes); /// /// // No mode is active: /// assert!(app.dispatch_bind_match(&"@left".into()).is_none()); /// assert!(app.dispatch_bind_match(&"@enter".into()).is_none()); /// assert!(app.dispatch_bind_match(&"@escape".into()).is_none()); /// assert!(app.dispatch_bind_match(&"@down".into()).is_none()); /// assert!(app.dispatch_bind_match(&"@up".into()).is_none()); /// /// // Push mode onto stack: /// app.mode_stack_push("Test"); /// assert!(app.dispatch_bind_match(&"@left".into()).is_none()); /// assert!(app.dispatch_bind_match(&"@enter".into()).is_some()); /// assert!(app.dispatch_bind_match(&"@escape".into()).is_some()); /// assert!(app.dispatch_bind_match(&"@down".into()).is_some()); /// assert!(app.dispatch_bind_match(&"@up".into()).is_some()); /// ``` fn dispatch_bind_match (&self, input: &TuiEvent) -> Option>]>> { for name in self.mode_stack().read().iter().rev() { if let Some(mode) = self.modes_get(name).as_ref().map(Arc::clone) { if let Some(binds) = self.dispatch_bind_match_axis(mode.as_ref(), input) { return Some(binds) } if let Some(binds) = self.dispatch_bind_match_keys(mode.as_ref(), input) { return Some(binds) } } } None } /// ``` /// # use ::{std::sync::Arc, tek::*}; /// let app = config_reload(App::default(), stringify!( /// (axis Y @up @down) (mode Test (axis Y Index)) /// )).unwrap(); /// assert!(app.dispatch_bind_match(&"@down".into()).is_none()); /// assert!(app.dispatch_bind_match(&"@up".into()).is_none()); /// app.mode_stack_push("Test"); /// assert!(app.dispatch_bind_match(&"@down".into()).is_some()); /// assert!(app.dispatch_bind_match(&"@up".into()).is_some()); /// ``` fn dispatch_bind_match_axis (&self, mode: &Mode, input: &TuiEvent) -> Option>]>> { let axis = self.binds_axis().read(); for (ctrl, _param) in mode.axis.iter() { if let Some(axis) = axis.get(ctrl) { if *input == axis.0 { return Some([Binding { commands: [].into(), ..Default::default() }].into()) } else if *input == axis.1 { return Some([Binding { commands: [].into(), ..Default::default() }].into()) } } } None } /// ``` /// # use ::{std::sync::Arc, tek::*}; /// let app = config_reload(App::default(), stringify!( /// (bind Confirm (@enter confirm)) (mode Test (bind Confirm (@escape Cancel))) /// )).unwrap(); /// assert!(app.dispatch_bind_match(&"@enter".into()).is_none()); /// assert!(app.dispatch_bind_match(&"@escape".into()).is_none()); /// app.mode_stack_push("Test"); /// assert!(app.dispatch_bind_match(&"@enter".into()).is_some()); /// assert!(app.dispatch_bind_match(&"@escape".into()).is_some()); /// ``` fn dispatch_bind_match_keys (&self, mode: &Mode, input: &TuiEvent) -> Option>]>> { for id in mode.keys.iter() { println!("{input:?} {id:?}"); if let Some(event_map) = self.binds_find(id) && let Some(bindings) = event_map.query(input) { return Some(bindings.into()) } if let Ok(Some(expr)) = id.expr() && let Ok(Some(head)) = expr.head() && *input == head.into() && let Ok(Some(tail)) = expr.tail() { return Some([Binding { commands: [tail.into()].into(), ..Default::default() }].into()) } } None } fn dispatch_bind_exec (&mut self, bindings: Arc<[Binding>]>) -> Usually<()> { for binding in bindings.iter() { for src in binding.commands.iter() { if let Some(command) = Namespace::::namespace(self, &src)? { self.cmd_dispatch(command)?; } } } Ok(()) } fn cmd_dispatch (&mut self, command: C) -> Usually<()>; } impl DispatchBind for App { fn cmd_dispatch (&mut self, command: AppCmd) -> Usually<()> { match command.clone().dispatch(self) { Ok(undo) => { self.history.push((command, undo)); Ok(()) }, Err(err) => { self.history.push((command, None)); Err(err) }, } } } pub use self::conf::*; mod conf { use crate::*; mod axis; pub use self::axis::*; mod bind; pub use self::bind::*; mod lang; pub use self::lang::*; mod mode; pub use self::mode::*; mod view; pub use self::view::*; pub trait HasError { fn error (&self) -> &RwLock>; fn error_set (&self, e: Option) { *self.error().write() = e; } } impl> HasError> for T { fn error (&self) -> &RwLock>> { &self.as_ref().error } } impl HasError> for Arc { fn error (&self) -> &RwLock>> { &self.as_ref().config.error } } pub trait HasConfig { fn config (&self) -> &Config; /// Print the configuration. fn config_print (&self) { let config = self.config(); print_config_views(config); print_config_binds(config); print_config_modes(config); } /// Make this configuration empty. fn config_clear (&self) { let config = self.config(); config.modes_clear(); config.views_clear(); config.binds_clear(); config.stamp.store(quanta::Clock::new().raw(), Relaxed); } } impl HasConfig for App { fn config (&self) -> &Config { &self.watch.config } } impl HasConfig for Watch { fn config (&self) -> &Config { &self.config } } impl HasConfig for Config { fn config (&self) -> &Config { self } } /// Default configuration contents. const CONFIG_DEFAULT: &'static str = include_str!("tek.edn"); pub fn config_init + AsRef> (config: C) -> Usually { // Write initial contents of configuration: let watcher: &Watch = config.as_ref(); if watcher.find_file().is_none() { std::fs::write(watcher.place_file()?, CONFIG_DEFAULT)?; } // Load config from file: Ok(if let Some(path) = watcher.find_file() { config_reload(config, std::fs::read_to_string(&path)?)? } else { return Err(format!("config_init: not found").into()) }) } /// Reload configuration from source. pub fn config_reload , L: Language> (state: C, src: L) -> Usually { state.as_ref().config_clear(); src.each(state, |c, s|config_add(c, s.trim())) } /// Load configuration item. fn config_add , L: Language> (state: C, src: L) -> Usually { if let Some(expr) = src.expr()? { match expr.head()? { Some("mode") => state.as_ref().modes_add(expr.tail()), Some("view") => state.as_ref().views_add(expr.tail()), Some("bind") => state.as_ref().binds_def_keys(expr.tail()), Some("axis") => state.as_ref().binds_def_axis(expr.tail()), _ => return Err(format!("config_add: unexpected: {src:?}").into()) }?; Ok(state) } else { Err(format!("Config::add_one: tried to add empty item").into()) } } /// Contains timestamped mode, view, and bind definitions. /// /// ``` /// # use ::{std::sync::Arc, tek::*}; /// /// // Default empty config: /// let config: Config = Default::default(); /// /// // Define a binding: /// config.binds_def_keys(stringify!(Confirm (@enter confirm))).unwrap(); /// /// // Define an axis: /// config.binds_def_axis(stringify!(Y @up @down)).unwrap(); /// ``` #[derive(Default, Debug)] pub struct Config { /// Error caught during (re)loading pub error: RwLock>>, /// Timestamp pub stamp: AtomicU64, /// Active collection of interaction modes. pub modes: Arc, Arc>>>, /// Active collection of view definitions. pub views: Arc, Arc>>>>, /// Active collection of key bindings. pub keys: Arc, Arc>>>>>, /// Active collection of axis bindings. pub axis: Arc, Arc>>>>, } impl Config { /// Create a new timestamped config state. pub fn new () -> Self { Self { stamp: quanta::Clock::new().raw().into(), ..Default::default() } } } } pub use self::gear::*; mod gear { mod device; pub use self::device::*; mod midi; pub use self::midi::*; mod mixer; pub use self::mixer::*; mod pool; pub use self::pool::*; mod sample; pub use self::sample::*; mod track; pub use self::track::*; #[cfg(feature = "lv2")] mod lv2; #[cfg(feature = "lv2")] pub use self::lv2::*; #[cfg(feature = "lv2_gui")] mod lv2_gui; #[cfg(feature = "lv2_gui")] pub use self::lv2_gui::*; #[cfg(feature = "vst2")] mod vst2; #[cfg(feature = "vst2")] pub use self::vst2::*; } #[cfg(feature = "cli")] pub use self::cli::*; #[cfg(feature = "cli")] mod cli { use super::*; /// Banner. pub(crate) const HEADER: &'static str = r#" ~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ █ █▀ █▀▀▄ ~ heatwave is the new darkwave ~ ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; pub fn show_version () { println!("versions aint real man"); } pub fn main_cli (config: Arc) -> Usually<()> { Cli::parse().run(Some(config.into())) } /// The command-line interface descriptor. /// /// ``` /// let cli: tek::Cli = Default::default(); /// /// use clap::CommandFactory; /// tek::Cli::command().debug_assert(); /// ``` #[derive(Parser, Debug, Default)] #[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] pub struct Cli { /// Pre-defined configuration modes. /// /// TODO: Replace these with scripted configurations. #[command(subcommand)] pub action: Action, /// Record data for performance flamegraph. #[arg(long)] trace: bool, } /// Command-line configuration. impl Cli { pub fn run (&self, mut config: Option>) -> Usually<()> { self.action.run( config.unwrap_or_else(||Arc::new(Watch::new(None))), self.trace ) } } /// Application modes that can be passed to the mommand line interface. /// /// ``` /// let action: tek::Action = Default::default(); /// ``` #[derive(Debug, Clone, Subcommand, Default)] pub enum Action { /// Continue where you left off #[default] Resume, /// Run headlessly in current session. Headless, /// Show status of current session. Status, /// List known sessions. List, /// Continue work in a copy of the current session. Fork, /// Create a new empty session. New(ProjectInit), /// Import media as new session. Import, /// Show configuration. Config, /// Show version. Version, } impl Action { fn run (&self, watch: Arc, _trace: bool) -> Usually<()> { use Action::*; match self { Version => show_version(), Config => watch.config_print(), Resume => todo!("resume session"), List => todo!("list sessions"), New(sesh) => Exit::run(|exit|Tui::run_main( exit.clone(), { #[allow(unused_mut)] let mut app = App::new(Some(exit), sesh.init()?, watch, "Menu"); app.dialog = Dialog::welcome(); #[cfg(feature = "prof2")] { if trace { app.guard = Some({ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; //let tracy = tracing_tracy::TracyLayer::default(); let (flame, _guard) = tracing_flame::FlameLayer::with_file("./tracing.folded").unwrap(); //let registry = tracing_subscriber::registry().with(flame).init(); //tracing::subscriber::set_global_default(registry)?; tracing::subscriber::set_global_default(tracing_subscriber::registry().with(flame))?; _guard }) } } Arc::new(RwLock::new(app)) })).map(|_|())?, _ => todo!() } 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) } } 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 } } /// Define and implement a unit of time macro_rules! impl_time_unit { ($T:ident) => { impl TimeUnit for $T {} impl_op!($T, Add, add, |a, b|{a + b}); impl_op!($T, Sub, sub, |a, b|{a - b}); impl_op!($T, Mul, mul, |a, b|{a * b}); impl_op!($T, Div, div, |a, b|{a / b}); impl_op!($T, Rem, rem, |a, b|{a % b}); impl_from!($T: |value: f64| { Self(value.into()) }); impl_from!($T: |value: usize| { Self((value as f64).into()) }); impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } } impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } } impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } } impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } } impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } impl $T { pub fn get (&self) -> f64 { self.0.load(Relaxed) } pub fn set (&self, value: f64) -> f64 { let old = self.get(); self.0.store(value, Relaxed); old } } } } impl_time_unit!(SampleCount); impl_time_unit!(SampleRate); impl_time_unit!(Microsecond); impl_time_unit!(Quantize); impl_time_unit!(Ppq); impl_time_unit!(Pulse); impl_time_unit!(Bpm); impl_time_unit!(LaunchSync); /// Define a type alias for iterators of sized items (columns). macro_rules! def_sizes_iter { ($Type:ident => $($Item:ty),+) => { pub trait $Type<'a>: Iterator + Send + Sync + 'a {} impl<'a, T: Iterator + Send + Sync + 'a> $Type<'a> for T {} } } def_sizes_iter!(ScenesSizes => Scene); def_sizes_iter!(TracksSizes => Track); def_sizes_iter!(PortsSizes => Arc, [Connect]); impl_has!(Clock: |self: Track|self.sequencer.clock); impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ)); primitive!(isize: try_to_isize); primitive!(u16: try_to_u16); primitive!(u8: try_to_u8); primitive!(usize: try_to_usize); impl_as_ref!(Config: |self: Config| self); impl_as_ref!(Config: |self: Watch| &self.config); impl_as_mut!(Config: |self: Watch| &mut self.config); impl_as_ref!(Watch: |self: Watch| &self); impl_debug!(MenuItem: |self, w| { write!(w, "{}", &self.0) }); impl_default!(MenuItem: Self::new("", [])); 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.watch.config.as_ref()); impl_as_mut!(Option: |self: App|self.project.as_mut()); impl_as_mut!(Option: |self: App|todo!()); impl_as_ref!(Option: |self: App|self.project.as_ref()); impl_as_ref!(Option: |self: App|todo!()); impl_as_mut_opt!(Scene: |self: App|self.project.as_mut_opt()); impl_as_mut_opt!(Track: |self: App|self.project.as_mut_opt()); impl_as_ref_opt!(Scene: |self: App|self.project.as_ref_opt()); impl_as_ref_opt!(Track: |self: App|self.project.as_ref_opt()); impl_audio!(App: tek_jack_process, tek_jack_event); impl_has!(Clock: |self: App|self.project.clock); impl_has!(Dialog: |self: App|self.dialog); impl_has!(Jack<'static>: |self: App|self.jack); impl_has!(Pool: |self: App|self.project.clips); impl_has!(Pool: |self: App|self.project.samples); impl_has!(Selection: |self: App|self.project.selection); impl_has!(Sizer: |self: App|self.size); impl_has!(Vec: |self: App|self.project.midi_ins); impl_has!(Vec: |self: App|self.project.midi_outs); impl_from!(AppCmd: |x: BrowseCmd| AppCmd::Browse { command: x }); impl_from!(AppCmd: |x: MidiClipCmd| AppCmd::MidiClip { command: x }); impl_from!(AppCmd: |x: MidiEditCmd| AppCmd::MidiEdit { command: x }); impl_from!(AppCmd: |x: ProjectCmd| AppCmd::Project { command: x }); impl_from!(AppCmd: |x: SceneCmd| AppCmd::Scene { command: x }); impl_from!(AppCmd: |x: ScenesCmd| AppCmd::Scenes { command: x }); impl_from!(AppCmd: |x: TrackCmd| AppCmd::Track { command: x }); impl_from!(AppCmd: |x: TracksCmd| AppCmd::Tracks { command: x }); tui_keys!(self: App, input { #[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!"); self.dispatch_bind(input) }); /// The [Draw] implementation for [App] handles the loaded view, /// which is defined in terms of [dizzle] DSL. /// /// If there is an error, the error is displayed. FIXME: overlay it /// Then, every top-level form of the DSL description is rendered. impl Draw for App { fn draw (&self, to: &mut Tui) -> Drawn { #[cfg(feature = "prof2")] profiling::scope!("App::draw"); self.perf.cycle(&mut |_|{ draw_error(to, self.error.as_ref())?; draw_mode_stack(to, &self)?; view_debug(&*self.stack.read(), &self.perf, &self.size).draw(to)?; #[cfg(feature = "prof2")] profiling::finish_frame!(); Ok(Some(to.area().into())) }) } }