diff --git a/app/arrange.rs b/app/arrange.rs new file mode 100644 index 00000000..0034912d --- /dev/null +++ b/app/arrange.rs @@ -0,0 +1,416 @@ + +/// Represents the current user selection in the arranger +#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { + #[default] + /// Nothing is selected + Nothing, + /// The whole mix is selected + Mix, + /// A MIDI input is selected. + Input(usize), + /// A MIDI output is selected. + Output(usize), + /// A scene is selected. + #[cfg(feature = "scene")] Scene(usize), + /// A track is selected. + #[cfg(feature = "track")] Track(usize), + /// A clip (track × scene) is selected. + #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, + /// A track's MIDI input connection is selected. + #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, + /// A track's MIDI output connection is selected. + #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, + /// A track device slot is selected. + #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, +} + +/// A scene consists of a set of clips to play together. +/// +/// ``` +/// let scene: tek::Scene = Default::default(); +/// let _ = scene.pulses(); +/// let _ = scene.is_playing(&[]); +/// ``` +#[derive(Debug, Default)] pub struct Scene { + /// Name of scene + pub name: Arc, + /// Identifying color of scene + pub color: ItemTheme, + /// Clips in scene, one per track + pub clips: Vec>>>, +} + +/// Arranger. +/// +/// ``` +/// let arranger = tek::Arrangement::default(); +/// ``` +#[derive(Default, Debug)] pub struct Arrangement { + /// Project name. + pub name: Arc, + /// Base color. + pub color: ItemTheme, + /// JACK client handle. + pub jack: Jack<'static>, + /// FIXME a render of the project arrangement, redrawn on update. + /// TODO rename to "render_cache" or smth + pub arranger: Arc>, + /// Display size + pub size: Measure, + /// Display size of clips area + pub size_inner: Measure, + /// 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, + /// Selected UI element + #[cfg(feature = "select")] pub selection: Selection, + /// 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 scene_scroll: usize, +} + +pub trait ClipsView: TracksView + ScenesView { + + fn view_scenes_clips <'a> (&'a self) + -> impl Draw + 'a + { + self.clips_size().of(wh_full(above( + wh_full(origin_se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), + Thunk::new(|to: &mut Tui|for ( + track_index, track, _, _ + ) in self.tracks_with_sizes() { + to.place(&w_exact(track.width as u16, + h_full(self.view_track_clips(track_index, track)))) + })))) + } + + fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw + 'a { + Thunk::new(move|to: &mut Tui|for ( + scene_index, scene, .. + ) in self.scenes_with_sizes() { + let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { + let clip = clip.read().unwrap(); + (format!(" ⏹ {}", &clip.name).into(), clip.color) + } else { + (" ⏹ -- ".into(), ItemTheme::G[32]) + }; + let fg = theme.lightest.rgb; + let mut outline = theme.base.rgb; + let bg = if self.selection().track() == Some(track_index) + && self.selection().scene() == Some(scene_index) + { + outline = theme.lighter.rgb; + theme.light.rgb + } else if self.selection().track() == Some(track_index) + || self.selection().scene() == Some(scene_index) + { + outline = theme.darkest.rgb; + theme.base.rgb + } else { + theme.dark.rgb + }; + let w = if self.selection().track() == Some(track_index) + && let Some(editor) = self.editor () + { + (editor.measure_width() as usize).max(24).max(track.width) + } else { + track.width + } as u16; + let y = if self.selection().scene() == Some(scene_index) + && let Some(editor) = self.editor () + { + (editor.measure_height() as usize).max(12) + } else { + Self::H_SCENE as usize + } as u16; + + to.place(&wh_exact(w, y, below( + wh_full(Outer(true, Style::default().fg(outline))), + wh_full(below( + below( + Tui::fg_bg(outline, bg, wh_full("")), + wh_full(origin_nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))), + ), + wh_full(when(self.selection().track() == Some(track_index) + && self.selection().scene() == Some(scene_index) + && self.is_editing(), self.editor()))))))); + }) + } + +} + +pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured { + + fn tracks_width_available (&self) -> u16 { + (self.measure_width() as u16).saturating_sub(40) + } + + /// Iterate over tracks with their corresponding sizes. + fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { + let _editor_width = self.editor().map(|e|e.measure_width()); + let _active_track = self.selection().track(); + let mut x = 0; + self.tracks().iter().enumerate().map_while(move |(index, track)|{ + let width = track.width.max(8); + if x + width < self.clips_size().w() as usize { + let data = (index, track, x, x + width); + x += width + Self::TRACK_SPACING; + Some(data) + } else { + None + } + }) + } + + fn view_track_names (&self, theme: ItemTheme) -> impl Draw { + let track_count = self.tracks().len(); + let scene_count = self.scenes().len(); + let selected = self.selection(); + let button = south( + button_3("t", "rack ", format!("{}{track_count}", selected.track() + .map(|track|format!("{track}/")).unwrap_or_default()), false), + button_3("s", "cene ", format!("{}{scene_count}", selected.scene() + .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); + let button_2 = south( + button_2("T", "+", false), + button_2("S", "+", false)); + view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb, + h_exact(2, Thunk::new(|to: &mut Tui|{ + for (index, track, x1, _x2) in self.tracks_with_sizes() { + to.place(&x_push(x1 as u16, w_exact(track_width(index, track), + Tui::bg(if selected.track() == Some(index) { + track.color.light.rgb + } else { + track.color.base.rgb + }, south(w_full(origin_nw(east( + format!("·t{index:02} "), + Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name)) + ))), ""))) ));}})))) + } + + fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw { + view_track_row_section(theme, + south(w_full(origin_w(button_2("o", "utput", false))), + Thunk::new(|to: &mut Tui|for port in self.midi_outs().iter() { + to.place(&w_full(origin_w(port.port_name()))); + })), + button_2("O", "+", false), + Tui::bg(theme.darker.rgb, origin_w(Thunk::new(|to: &mut Tui|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&w_exact(track_width(index, track), + origin_nw(h_full(iter_south(1, ||track.sequencer.midi_outs.iter(), + |port, index|Tui::fg(Rgb(255, 255, 255), + h_exact(1, Tui::bg(track.color.dark.rgb, w_full(origin_w( + format!("·o{index:02} {}", port.port_name())))))))))));}})))) + } + + fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw { + let mut h = 0u16; + for track in self.tracks().iter() { + h = h.max(track.sequencer.midi_ins.len() as u16); + } + let content = Thunk::new(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&wh_exact(track_width(index, track), h + 1, + origin_nw(south( + Tui::bg(track.color.base.rgb, + w_full(origin_w(east!( + either(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "), + either(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "), + either(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "), + )))), + iter_south(1, ||track.sequencer.midi_ins.iter(), + |port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb, + w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))); + }); + view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), + Tui::bg(theme.darker.rgb, origin_w(content))) + } + +} + +pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync { + /// Default scene height. + const H_SCENE: usize = 2; + /// Default editor height. + const H_EDITOR: usize = 15; + fn h_scenes (&self) -> u16; + fn w_side (&self) -> u16; + fn w_mid (&self) -> u16; + fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> { + let mut y = 0; + self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{ + let height = if self.selection().scene() == Some(s) && self.editor().is_some() { + 8 + } else { + Self::H_SCENE + }; + if y + height <= self.clips_size().h() as usize { + let data = (s, scene, y, y + height); + y += height; + Some(data) + } else { + None + } + }) + } + + fn view_scenes_names (&self) -> impl Draw { + w_exact(20, Thunk::new(|to: &mut Tui|for (index, scene, ..) in self.scenes_with_sizes() { + to.place(&self.view_scene_name(index, scene)); + })) + } + + fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw + 'a { + let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() { + 7 + } else { + Self::H_SCENE as u16 + }; + let bg = if self.selection().scene() == Some(index) { + scene.color.light.rgb + } else { + scene.color.base.rgb + }; + let a = w_full(origin_w(east(format!("·s{index:02} "), + Tui::fg(Tui::g(255), Tui::bold(true, &scene.name))))); + let b = when(self.selection().scene() == Some(index) && self.is_editing(), + wh_full(origin_nw(south( + self.editor().as_ref().map(|e|e.clip_status()), + self.editor().as_ref().map(|e|e.edit_status()))))); + wh_exact(20, h, Tui::bg(bg, origin_nw(south(a, b)))) + } + +} +pub trait HasSceneScroll: HasScenes { fn scene_scroll (&self) -> usize; } +pub trait HasTrackScroll: HasTracks { fn track_scroll (&self) -> usize; } +pub trait HasScene: AsRefOpt + AsMutOpt { + fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() } + fn scene (&self) -> Option<&Scene> { self.as_ref_opt() } +} +pub trait HasSelection: AsRef + AsMut { + fn selection (&self) -> &Selection { self.as_ref() } + fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } + /// Get the active track + #[cfg(feature = "track")] + fn selected_track (&self) -> Option<&Track> where Self: HasTracks { + let index = self.selection().track()?; + self.tracks().get(index) + } + /// Get a mutable reference to the active track + #[cfg(feature = "track")] + fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { + let index = self.selection().track()?; + self.tracks_mut().get_mut(index) + } + /// Get the active scene + #[cfg(feature = "scene")] + fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { + let index = self.selection().scene()?; + self.scenes().get(index) + } + /// Get a mutable reference to the active scene + #[cfg(feature = "scene")] + fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { + let index = self.selection().scene()?; + self.scenes_mut().get_mut(index) + } + /// Get the active clip + #[cfg(feature = "clip")] + fn selected_clip (&self) -> Option>> where Self: HasScenes + HasTracks { + self.selected_scene()?.clips.get(self.selection().track()?)?.clone() + } +} +pub trait HasScenes: AsRef> + AsMut> { + fn scenes (&self) -> &Vec { self.as_ref() } + fn scenes_mut (&mut self) -> &mut Vec { self.as_mut() } + /// Generate the default name for a new scene + fn scene_default_name (&self) -> Arc { format!("s{:3>}", self.scenes().len() + 1).into() } + fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) } + /// Add multiple scenes + fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks { + let scene_color_1 = ItemColor::random(); + let scene_color_2 = ItemColor::random(); + for i in 0..n { + let _ = self.scene_add(None, Some( + scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() + ))?; + } + Ok(()) + } + /// Add a scene + fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<(usize, &mut Scene)> where Self: HasTracks + { + let scene = Scene { + name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), + clips: vec![None;self.tracks().len()], + color: color.unwrap_or_else(ItemTheme::random), + }; + self.scenes_mut().push(scene); + let index = self.scenes().len() - 1; + Ok((index, &mut self.scenes_mut()[index])) + } +} +pub trait HasTracks: AsRef> + AsMut> { + fn tracks (&self) -> &Vec { self.as_ref() } + fn tracks_mut (&mut self) -> &mut Vec { self.as_mut() } + /// Run audio callbacks for every track and every device + fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { + for track in self.tracks_mut().iter_mut() { + if Control::Quit == Audio::process(&mut track.sequencer, client, scope) { + return Control::Quit + } + for device in track.devices.iter_mut() { + if Control::Quit == DeviceAudio(device).process(client, scope) { + return Control::Quit + } + } + } + Control::Continue + } + fn track_longest_name (&self) -> usize { self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) } + /// Stop all playing clips + fn tracks_stop_all (&mut self) { for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } } + /// Stop all playing clips + fn tracks_launch (&mut self, clips: Option>>>>) { + if let Some(clips) = clips { + for (clip, track) in clips.iter().zip(self.tracks_mut()) { track.sequencer.enqueue_next(clip.as_ref()); } + } else { + for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } + } + } + /// Spacing between tracks. + const TRACK_SPACING: usize = 0; +} +pub trait HasTrack: AsRefOpt + AsMutOpt { + fn track (&self) -> Option<&Track> { self.as_ref_opt() } + fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() } + #[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw + 'a { + self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) + } + #[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw + '_ { + self.track().map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) + } + #[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw { + self.track().map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) + } + #[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw { + self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) + } +} diff --git a/app/browse.rs b/app/browse.rs new file mode 100644 index 00000000..ccf7e37c --- /dev/null +++ b/app/browse.rs @@ -0,0 +1,97 @@ +/// 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: Measure, +} + +pub(crate) struct EntriesIterator<'a> { + pub browser: &'a Browse, + pub offset: usize, + pub length: usize, + pub index: usize, +} + +#[derive(Clone, Debug)] pub enum BrowseTarget { + SaveProject, + LoadProject, + ImportSample(Arc>>), + ExportSample(Arc>>), + ImportClip(Arc>>), + ExportClip(Arc>>), +} + +/// A clip pool. +/// +/// ``` +/// let pool = tek::Pool::default(); +/// ``` +#[derive(Debug)] pub struct Pool { + pub visible: bool, + /// Selected clip + pub clip: AtomicUsize, + /// Mode switch + pub mode: Option, + /// Embedded file browse + #[cfg(feature = "browse")] pub browse: Option, + /// Collection of MIDI clips. + #[cfg(feature = "clip")] pub clips: Arc>>>>, + /// Collection of sound samples. + #[cfg(feature = "sampler")] pub samples: Arc>>>>, +} + +/// 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, +} + +/// Some sort of wrapper again? +pub struct PoolView<'a>(pub &'a Pool); + +// Commands supported by [Browse] +//#[derive(Debug, Clone, PartialEq)] +//pub enum BrowseCommand { + //Begin, + //Cancel, + //Confirm, + //Select(usize), + //Chdir(PathBuf), + //Filter(Arc), +//} + +/// 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, +} diff --git a/app/connect.rs b/app/connect.rs new file mode 100644 index 00000000..97bae48b --- /dev/null +++ b/app/connect.rs @@ -0,0 +1,306 @@ +use crate::*; + +/// Audio input port. +#[derive(Debug)] pub struct AudioInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +/// Audio output port. +#[derive(Debug)] pub struct AudioOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +/// MIDI input port. +#[derive(Debug)] pub struct MidiInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of currently held notes. + pub held: Arc>, + /// List of ports to connect to. + pub connections: Vec, +} + +/// MIDI output port. +#[derive(Debug)] pub struct MidiOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, + /// List of currently held notes. + pub held: Arc>, + /// Buffer + pub note_buffer: Vec, + /// Buffer + pub output_buffer: Vec>>, +} + +#[derive(Clone, Debug, PartialEq)] pub enum ConnectName { + /** Exact match */ + Exact(Arc), + /** Match regular expression */ + RegExp(Arc), +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { + One, + All +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { + Missing, + Disconnected, + Connected, + Mismatch, +} + +/// Port connection manager. +/// +/// ``` +/// let connect = tek::Connect::default(); +/// ``` +#[derive(Clone, Debug, Default)] pub struct Connect { + pub name: Option, + pub scope: Option, + pub status: Arc, Arc, ConnectStatus)>>>, + pub info: Arc, +} + +impl Connect { + pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) + -> Vec + { + let mut connections = vec![]; + for port in exact.iter() { connections.push(Self::exact(port)) } + for port in re.iter() { connections.push(Self::regexp(port)) } + for port in re_all.iter() { connections.push(Self::regexp_all(port)) } + connections + } + /// Connect to this exact port + pub fn exact (name: impl AsRef) -> Self { + let info = format!("=:{}", name.as_ref()).into(); + let name = Some(Exact(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp (name: impl AsRef) -> Self { + let info = format!("~:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp_all (name: impl AsRef) -> Self { + let info = format!("+:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn info (&self) -> Arc { + format!(" ({}) {} {}", { + let status = self.status.read().unwrap(); + let mut ok = 0; + for (_, _, state) in status.iter() { + if *state == Connected { + ok += 1 + } + } + format!("{ok}/{}", status.len()) + }, match self.scope { + None => "x", + Some(One) => " ", + Some(All) => "*", + }, match &self.name { + None => format!("x"), + Some(Exact(name)) => format!("= {name}"), + Some(RegExp(name)) => format!("~ {name}"), + }).into() + } +} + +impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl> RegisterPorts for J { + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiInput::new(self.jack(), name, connect) + } + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiOutput::new(self.jack(), name, connect) + } + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioInput::new(self.jack(), name, connect) + } + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioOutput::new(self.jack(), name, connect) + } +} + +/// May create new MIDI input ports. +pub trait AddMidiIn { + fn midi_in_add (&mut self) -> Usually<()>; +} + +/// May create new MIDI output ports. +pub trait AddMidiOut { + fn midi_out_add (&mut self) -> Usually<()>; +} + +pub trait RegisterPorts: HasJack<'static> { + /// Register a MIDI input port. + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register a MIDI output port. + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio input port. + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio output port. + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; +} + +pub trait JackPort: HasJack<'static> { + + type Port: PortSpec + Default; + + type Pair: PortSpec + Default; + + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized; + + fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { + jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) + .map_err(|e|e.into()) + } + + fn port_name (&self) -> &Arc; + + fn connections (&self) -> &[Connect]; + + fn port (&self) -> &Port; + + fn port_mut (&mut self) -> &mut Port; + + fn into_port (self) -> Port where Self: Sized; + + fn close (self) -> Usually<()> where Self: Sized { + let jack = self.jack().clone(); + Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) + } + + fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { + self.with_client(|c|c.ports(re_name, re_type, flags)) + } + + fn port_by_id (&self, id: u32) -> Option> { + self.with_client(|c|c.port_by_id(id)) + } + + fn port_by_name (&self, name: impl AsRef) -> Option> { + self.with_client(|c|c.port_by_name(name.as_ref())) + } + + fn connect_to_matching <'k> (&'k self) -> Usually<()> { + for connect in self.connections().iter() { + match &connect.name { + Some(Exact(name)) => { + *connect.status.write().unwrap() = self.connect_exact(name)?; + }, + Some(RegExp(re)) => { + *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; + }, + _ => {}, + }; + } + Ok(()) + } + + fn connect_exact <'k> (&'k self, name: &str) -> + Usually, Arc, ConnectStatus)>> + { + self.with_client(move|c|{ + let mut status = vec![]; + for port in c.ports(None, None, PortFlags::empty()).iter() { + if port.as_str() == &*name { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected { + break + } + } + } + } + Ok(status) + }) + } + + fn connect_regexp <'k> ( + &'k self, re: &str, scope: Option + ) -> Usually, Arc, ConnectStatus)>> { + self.with_client(move|c|{ + let mut status = vec![]; + let ports = c.ports(Some(&re), None, PortFlags::empty()); + for port in ports.iter() { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected && scope == Some(One) { + break + } + } + } + Ok(status) + }) + } + + /** Connect to a matching port by name. */ + fn connect_to_name (&self, name: impl AsRef) -> Usually { + self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { + self.connect_to_unowned(port) + } else { + Ok(Missing) + }) + } + + /** Connect to a matching port by reference. */ + fn connect_to_unowned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } + + /** Connect to an owned matching port by reference. */ + fn connect_to_owned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } + +} diff --git a/app/device.rs b/app/device.rs new file mode 100644 index 00000000..e69de29b diff --git a/app/mix.rs b/app/mix.rs new file mode 100644 index 00000000..63248378 --- /dev/null +++ b/app/mix.rs @@ -0,0 +1,14 @@ +#[derive(Debug, Default)] pub enum MeteringMode { + #[default] Rms, + Log10, +} + +#[derive(Debug, Default, Clone)] pub struct Log10Meter(pub f32); + +#[derive(Debug, Default, Clone)] pub struct RmsMeter(pub f32); + +#[derive(Debug, Default)] pub enum MixingMode { + #[default] Summing, + Average, +} + diff --git a/app/plugin.rs b/app/plugin.rs new file mode 100644 index 00000000..01d287f4 --- /dev/null +++ b/app/plugin.rs @@ -0,0 +1,28 @@ + +/// A LV2 plugin. +#[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 { + /// JACK client handle (needs to not be dropped for standalone mode to work). + pub jack: Jack<'static>, + pub name: Arc, + pub path: Option>, + pub selected: usize, + pub mapping: bool, + pub midi_ins: Vec>, + pub midi_outs: Vec>, + pub audio_ins: Vec>, + pub audio_outs: Vec>, + + pub lv2_world: livi::World, + pub lv2_instance: livi::Instance, + pub lv2_plugin: livi::Plugin, + pub lv2_features: Arc, + pub lv2_port_list: Vec, + pub lv2_input_buffer: Vec, + pub lv2_ui_thread: Option>, +} + +/// A LV2 plugin's X11 UI. +#[cfg(feature = "lv2_gui")] pub struct LV2PluginUI { + pub window: Option +} + diff --git a/app/sample.rs b/app/sample.rs new file mode 100644 index 00000000..773644cc --- /dev/null +++ b/app/sample.rs @@ -0,0 +1,104 @@ +/// Plays [Voice]s from [Sample]s. +/// +/// ``` +/// let sampler = tek::Sampler::default(); +/// ``` +#[derive(Debug, Default)] pub struct Sampler { + /// Name of sampler. + pub name: Arc, + /// Device color. + pub color: ItemTheme, + /// Sample currently being recorded. + pub recording: Option<(usize, Option>>)>, + /// Recording buffer. + pub buffer: Vec>, + /// Samples mapped to MIDI notes. + pub samples: SampleKit<128>, + /// Collection of currently playing instances of samples. + pub voices: Arc>>, + /// Samples that are not mapped to MIDI notes. + pub unmapped: Vec>>, + /// Sample currently being edited. + pub editing: Option>>, + /// How to mix the voices. + pub mixing_mode: MixingMode, + /// How to meter the inputs and outputs. + pub metering_mode: MeteringMode, + /// Fixed gain applied to all output. + pub output_gain: f32, + /// Currently active modal, if any. + pub mode: Option, + /// Size of rendered sampler. + pub size: Measure, + /// Lowest note displayed. + pub note_lo: AtomicUsize, + /// Currently selected note. + pub note_pt: AtomicUsize, + /// Selected note as row/col. + pub cursor: (AtomicUsize, AtomicUsize), + /// Audio input meters. + #[cfg(feature = "meter")] pub input_meters: Vec, + /// Audio input ports. Samples are recorded from here. + #[cfg(feature = "port")] pub audio_ins: Vec, + /// MIDI input port. Sampler are triggered from here. + #[cfg(feature = "port")] pub midi_in: Option, + /// Audio output ports. Voices are played into here. + #[cfg(feature = "port")] pub audio_outs: Vec, + /// Audio output meters. + #[cfg(feature = "meter")] pub output_meters: Vec, +} + +/// Collection of samples, one per slot, fixed number of slots. +/// +/// History: Separated to cleanly implement [Default]. +/// +/// ``` +/// let samples = tek::SampleKit([None, None, None, None]); +/// ``` +#[derive(Debug)] pub struct SampleKit( + pub [Option>>;N] +); + +/// A sound cut. +/// +/// ``` +/// let sample = tek::Sample::default(); +/// let sample = tek::Sample::new("test", 0, 0, vec![]); +/// ``` +#[derive(Default, Debug)] pub struct Sample { + pub name: Arc, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, + pub gain: f32, + pub color: ItemTheme, +} + +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} + +#[derive(Default, Debug)] pub struct SampleAdd { + pub exited: bool, + pub dir: PathBuf, + pub subdirs: Vec, + pub files: Vec, + pub cursor: usize, + pub offset: usize, + pub sample: Arc>, + pub voices: Arc>>, + pub _search: Option, +} + +#[derive(Debug)] pub enum SamplerMode { + // Load sample from path + Import(usize, Browse), +} + +pub type MidiSample = + (Option, Arc>); diff --git a/app/sequence.rs b/app/sequence.rs new file mode 100644 index 00000000..049b6dae --- /dev/null +++ b/app/sequence.rs @@ -0,0 +1,596 @@ + +/// Contains state for viewing and editing a clip. +/// +/// ``` +/// use std::sync::{Arc, RwLock}; +/// let clip = tek::MidiClip::stop_all(); +/// let mut editor = tek::MidiEditor { +/// mode: tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip)))), +/// size: Default::default(), +/// //keys: Default::default(), +/// }; +/// let _ = editor.put_note(true); +/// let _ = editor.put_note(false); +/// let _ = editor.clip_status(); +/// let _ = editor.edit_status(); +/// ``` +pub struct MidiEditor { + /// Size of editor on screen + pub size: Measure, + /// View mode and state of editor + pub mode: PianoHorizontal, +} + +/// A clip, rendered as a horizontal piano roll. +/// +/// ``` +/// let piano = tek::PianoHorizontal::default(); +/// ``` +#[derive(Clone, Default)] pub struct PianoHorizontal { + pub clip: Option>>, + /// Buffer where the whole clip is rerendered on change + pub buffer: Arc>, + /// Size of actual notes area + pub size: Measure, + /// The display window + pub range: MidiSelection, + /// The note cursor + pub point: MidiCursor, + /// The highlight color palette + pub color: ItemTheme, + /// Width of the keyboard + pub keys_width: u16, +} + +/// 12 piano keys, some highlighted. +/// +/// ``` +/// let keys = tek::OctaveVertical::default(); +/// ``` +#[derive(Copy, Clone)] pub struct OctaveVertical { + pub on: [bool; 12], + pub colors: [Color; 3] +} +/// A MIDI sequence. +/// +/// ``` +/// let clip = tek::MidiClip::default(); +/// ``` +#[derive(Debug, Clone, Default)] pub struct MidiClip { + pub uuid: uuid::Uuid, + /// Name of clip + pub name: Arc, + /// Temporal resolution in pulses per quarter note + pub ppq: usize, + /// Length of clip in pulses + pub length: usize, + /// Notes in clip + pub notes: MidiData, + /// Whether to loop the clip or play it once + pub looped: bool, + /// Start of loop + pub loop_start: usize, + /// Length of loop + pub loop_length: usize, + /// All notes are displayed with minimum length + pub percussive: bool, + /// Identifying color of clip + pub color: ItemTheme, +} + +/// Contains state for playing a clip +/// +/// ``` +/// let clip = tek::MidiClip::default(); +/// println!("Empty clip: {clip:?}"); +/// +/// let clip = tek::MidiClip::stop_all(); +/// println!("Panic clip: {clip:?}"); +/// +/// let mut clip = tek::MidiClip::new("clip", true, 1, None, None); +/// clip.set_length(96); +/// clip.toggle_loop(); +/// clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); +/// assert!(clip.contains_note_on(36.into(), 6, 18)); +/// assert_eq!(&clip.notes, &clip.duplicate().notes); +/// +/// let clip = std::sync::Arc::new(clip); +/// assert_eq!(clip.clone(), clip); +/// +/// let sequencer = tek::Sequencer::default(); +/// println!("{sequencer:?}"); +/// ``` +pub struct Sequencer { + /// State of clock and playhead + #[cfg(feature = "clock")] pub clock: Clock, + /// Start time and clip being played + #[cfg(feature = "clip")] pub play_clip: Option<(Moment, Option>>)>, + /// Start time and next clip + #[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option>>)>, + /// Record from MIDI ports to current sequence. + #[cfg(feature = "port")] pub midi_ins: Vec, + /// Play from current sequence to MIDI ports + #[cfg(feature = "port")] pub midi_outs: Vec, + /// Play input through output. + pub monitoring: bool, + /// Write input to sequence. + pub recording: bool, + /// Overdub input to sequence. + pub overdub: bool, + /// Send all notes off + pub reset: bool, // TODO?: after Some(nframes) + /// Notes currently held at input + pub notes_in: Arc>, + /// Notes currently held at output + pub notes_out: Arc>, + /// MIDI output buffer + pub note_buf: Vec, + /// MIDI output buffer + pub midi_buf: Vec>>, +} + +/// A track consists of a sequencer and zero or more devices chained after it. +/// +/// ``` +/// let track: tek::Track = Default::default(); +/// ``` +#[derive(Debug, Default)] pub struct Track { + /// Name of track + pub name: Arc, + /// Identifying color of track + pub color: ItemTheme, + /// Preferred width of track column + pub width: usize, + /// MIDI sequencer state + pub sequencer: Sequencer, + /// Device chain + pub devices: Vec, +} + + +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>>)>; + + fn pulses_since_start (&self) -> Option { + if let Some((started, Some(_))) = self.play_clip().as_ref() { + let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); + return Some(elapsed) + } + None + } + + fn pulses_since_start_looped (&self) -> Option<(f64, f64)> { + if let Some((started, Some(clip))) = self.play_clip().as_ref() { + let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); + let length = clip.read().unwrap().length.max(1); // prevent div0 on empty clip + let times = (elapsed as usize / length) as f64; + let elapsed = (elapsed as usize % length) as f64; + return Some((times, elapsed)) + } + None + } + + fn enqueue_next (&mut self, clip: Option<&Arc>>) { + *self.next_clip_mut() = Some((self.clock().next_launch_instant(), clip.cloned())); + *self.reset_mut() = true; + } + + fn play_status (&self) -> impl Draw { + let (name, color): (Arc, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() { + let MidiClip { ref name, color, .. } = *clip.read().unwrap(); + (name.clone(), color) + } else { + ("".into(), Tui::g(64).into()) + }; + let time: String = self.pulses_since_start_looped() + .map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time))) + .unwrap_or_else(||String::from(" ")).into(); + field_v(color, "Now:", format!("{} {}", time, name)) + } + + fn next_status (&self) -> impl Draw { + let mut time: Arc = String::from("--.-.--").into(); + let mut name: Arc = String::from("").into(); + let mut color = ItemTheme::G[64]; + let clock = self.clock(); + if let Some((t, Some(clip))) = self.next_clip() { + let clip = clip.read().unwrap(); + name = clip.name.clone(); + color = clip.color.clone(); + time = { + let target = t.pulse.get(); + let current = clock.playhead.pulse.get(); + if target > current { + let remaining = target - current; + format!("-{:>}", clock.timebase.format_beats_1(remaining)) + } else { + String::new() + } + }.into() + } else if let Some((t, Some(clip))) = self.play_clip() { + let clip = clip.read().unwrap(); + if clip.looped { + name = clip.name.clone(); + color = clip.color.clone(); + let target = t.pulse.get() + clip.length as f64; + let current = clock.playhead.pulse.get(); + if target > current { + time = format!("-{:>}", clock.timebase.format_beats_0(target - current)).into() + } + } else { + name = "Stop".to_string().into(); + } + }; + field_v(color, "Next:", format!("{} {}", time, name)) + } +} + +pub trait MidiMonitor: HasMidiIns + HasMidiBuffers { + /// Input note flags. + fn notes_in (&self) -> &Arc>; + /// Current monitoring status. + fn monitoring (&self) -> bool; + /// Mutable monitoring status. + fn monitoring_mut (&mut self) -> &mut bool; + /// Enable or disable monitoring. + fn toggle_monitor (&mut self) { *self.monitoring_mut() = !self.monitoring(); } + /// Perform monitoring. + fn monitor (&mut self, _scope: &ProcessScope) { /* do nothing by default */ } +} + +pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { + fn recording (&self) -> bool; + fn recording_mut (&mut self) -> &mut bool; + fn toggle_record (&mut self) { + *self.recording_mut() = !self.recording(); + } + + fn overdub (&self) -> bool; + fn overdub_mut (&mut self) -> &mut bool; + fn toggle_overdub (&mut self) { + *self.overdub_mut() = !self.overdub(); + } + + fn record_clip ( + &mut self, + scope: &ProcessScope, + started: Moment, + clip: &Option>>, + ) { + if let Some(clip) = clip { + let sample0 = scope.last_frame_time() as usize; + let start = started.sample.get() as usize; + let _recording = self.recording(); + let timebase = self.clock().timebase().clone(); + let quant = self.clock().quant.get(); + let mut clip = clip.write().unwrap(); + let length = clip.length; + for input in self.midi_ins_mut().iter() { + for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) { + if let LiveEvent::Midi { message, .. } = event { + clip.record_event({ + let sample = (sample0 + sample - start) as f64; + let pulse = timebase.samples_to_pulse(sample); + let quantized = (pulse / quant).round() * quant; + quantized as usize % length + }, message); + } + } + } + } + } + + fn record_next (&mut self) { + // TODO switch to next clip and record into it + } +} + +pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize); + fn redraw (&self); + fn clip (&self) -> &Option>>; + fn clip_mut (&mut self) -> &mut Option>>; + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.redraw(); + } + /// Make sure cursor is within note range + fn autoscroll (&self) { + let note_pos = self.get_note_pos().min(127); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + if note_pos < note_lo { + self.note_lo().set(note_pos); + } else if note_pos > note_hi { + self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi)); + } + } + /// Make sure time range is within display + fn autozoom (&self) { + if self.time_lock().get() { + let time_len = self.get_time_len(); + let time_axis = self.get_time_axis(); + let time_zoom = self.get_time_zoom(); + loop { + let time_zoom = self.time_zoom().get(); + let time_area = time_axis * time_zoom; + if time_area > time_len { + let next_time_zoom = note_duration_prev(time_zoom); + if next_time_zoom <= 1 { + break + } + let next_time_area = time_axis * next_time_zoom; + if next_time_area >= time_len { + self.time_zoom().set(next_time_zoom); + } else { + break + } + } else if time_area < time_len { + let prev_time_zoom = note_duration_next(time_zoom); + if prev_time_zoom > 384 { + break + } + let prev_time_area = time_axis * prev_time_zoom; + if prev_time_area <= time_len { + self.time_zoom().set(prev_time_zoom); + } else { + break + } + } + } + if time_zoom != self.time_zoom().get() { + self.redraw() + } + } + //while time_len.div_ceil(time_zoom) > time_axis { + //println!("\r{time_len} {time_zoom} {time_axis}"); + //time_zoom = Note::next(time_zoom); + //} + //self.time_zoom().set(time_zoom); + } +} +pub type MidiData = + Vec>; +pub type ClipPool = + Vec>>; +pub type CollectedMidiInput<'a> = + Vec, MidiError>)>>; + +pub trait HasClips { + fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; + fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>; + fn add_clip (&self) -> (usize, Arc>) { + let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None))); + self.clips_mut().push(clip.clone()); + (self.clips().len() - 1, clip) + } +} +/// ``` +/// use tek::{*, tengri::*}; +/// +/// struct Test(Option); +/// impl_as_ref_opt!(MidiEditor: |self: Test|self.0.as_ref()); +/// impl_as_mut_opt!(MidiEditor: |self: Test|self.0.as_mut()); +/// +/// let mut host = Test(Some(MidiEditor::default())); +/// let _ = host.editor(); +/// let _ = host.editor_mut(); +/// let _ = host.is_editing(); +/// let _ = host.editor_w(); +/// let _ = host.editor_h(); +/// ``` +pub trait HasEditor: AsRefOpt + AsMutOpt { + fn editor (&self) -> Option<&MidiEditor> { self.as_ref_opt() } + fn editor_mut (&mut self) -> Option<&mut MidiEditor> { self.as_mut_opt() } + fn is_editing (&self) -> bool { self.editor().is_some() } + fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) as usize } + fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) as usize } +} +/// Trait for thing that may receive MIDI. +pub trait HasMidiIns { + fn midi_ins (&self) -> &Vec; + fn midi_ins_mut (&mut self) -> &mut Vec; + /// Collect MIDI input from app ports (TODO preallocate large buffers) + fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { + self.midi_ins().iter() + .map(|port|port.port().iter(scope) + .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) + .collect::>()) + .collect::>() + } + fn midi_ins_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_ins().iter().enumerate().map(move|(i, input)|{ + let height = 1 + input.connections().len(); + let data = (i, input.port_name(), input.connections(), y, y + height); + y += height; + data + }) + } +} +/// Trait for thing that may output MIDI. +pub trait HasMidiOuts { + fn midi_outs (&self) -> &Vec; + fn midi_outs_mut (&mut self) -> &mut Vec; + fn midi_outs_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_outs().iter().enumerate().map(move|(i, output)|{ + let height = 1 + output.connections().len(); + let data = (i, output.port_name(), output.connections(), y, y + height); + y += height; + data + }) + } + fn midi_outs_emit (&mut self, scope: &ProcessScope) { + for port in self.midi_outs_mut().iter_mut() { + port.buffer_emit(scope) + } + } +} +pub trait HasMidiClip { + fn clip (&self) -> Option>>; +} +pub trait HasSequencer: AsRef + AsMut { + fn sequencer_mut (&mut self) -> &mut Sequencer { self.as_mut() } + fn sequencer (&self) -> &Sequencer { self.as_ref() } +} +pub trait HasMidiBuffers { + fn note_buf_mut (&mut self) -> &mut Vec; + fn midi_buf_mut (&mut self) -> &mut Vec>>; +} + + +pub trait NotePoint { + fn note_len (&self) -> &AtomicUsize; + /// Get the current length of the note cursor. + fn get_note_len (&self) -> usize { + self.note_len().load(Relaxed) + } + /// Set the length of the note cursor, returning the previous value. + fn set_note_len (&self, x: usize) -> usize { + self.note_len().swap(x, Relaxed) + } + + fn note_pos (&self) -> &AtomicUsize; + /// Get the current pitch of the note cursor. + fn get_note_pos (&self) -> usize { + self.note_pos().load(Relaxed).min(127) + } + /// Set the current pitch fo the note cursor, returning the previous value. + fn set_note_pos (&self, x: usize) -> usize { + self.note_pos().swap(x.min(127), Relaxed) + } +} + +pub trait TimePoint { + fn time_pos (&self) -> &AtomicUsize; + /// Get the current time position of the note cursor. + fn get_time_pos (&self) -> usize { + self.time_pos().load(Relaxed) + } + /// Set the current time position of the note cursor, returning the previous value. + fn set_time_pos (&self, x: usize) -> usize { + self.time_pos().swap(x, Relaxed) + } +} + +pub trait MidiPoint: NotePoint + TimePoint { + /// Get the current end of the note cursor. + fn get_note_end (&self) -> usize { + self.get_time_pos() + self.get_note_len() + } +} + +pub trait TimeRange { + fn time_len (&self) -> &AtomicUsize; + fn get_time_len (&self) -> usize { + self.time_len().load(Ordering::Relaxed) + } + fn time_zoom (&self) -> &AtomicUsize; + fn get_time_zoom (&self) -> usize { + self.time_zoom().load(Ordering::Relaxed) + } + fn set_time_zoom (&self, value: usize) -> usize { + self.time_zoom().swap(value, Ordering::Relaxed) + } + fn time_lock (&self) -> &AtomicBool; + fn get_time_lock (&self) -> bool { + self.time_lock().load(Ordering::Relaxed) + } + fn set_time_lock (&self, value: bool) -> bool { + self.time_lock().swap(value, Ordering::Relaxed) + } + fn time_start (&self) -> &AtomicUsize; + fn get_time_start (&self) -> usize { + self.time_start().load(Ordering::Relaxed) + } + fn set_time_start (&self, value: usize) -> usize { + self.time_start().swap(value, Ordering::Relaxed) + } + fn time_axis (&self) -> &AtomicUsize; + fn get_time_axis (&self) -> usize { + self.time_axis().load(Ordering::Relaxed) + } + fn get_time_end (&self) -> usize { + self.time_start().get() + self.time_axis().get() * self.time_zoom().get() + } +} + +pub trait NoteRange { + fn note_lo (&self) -> &AtomicUsize; + fn get_note_lo (&self) -> usize { + self.note_lo().load(Ordering::Relaxed) + } + fn set_note_lo (&self, x: usize) -> usize { + self.note_lo().swap(x, Ordering::Relaxed) + } + fn note_axis (&self) -> &AtomicUsize; + fn get_note_axis (&self) -> usize { + self.note_axis().load(Ordering::Relaxed) + } + fn get_note_hi (&self) -> usize { + (self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127) + } +} + +pub trait MidiRange: TimeRange + NoteRange {} + +/// +/// ``` +/// let _ = tek::MidiCursor::default(); +/// ``` +#[derive(Debug, Clone)] pub struct MidiCursor { + /// Time coordinate of cursor + pub time_pos: Arc, + /// Note coordinate of cursor + pub note_pos: Arc, + /// Length of note that will be inserted, in pulses + pub note_len: Arc, +} + +/// +/// ``` +/// use tek::{TimeRange, NoteRange}; +/// let model = tek::MidiSelection::from((1, false)); +/// +/// let _ = model.get_time_len(); +/// let _ = model.get_time_zoom(); +/// let _ = model.get_time_lock(); +/// let _ = model.get_time_start(); +/// let _ = model.get_time_axis(); +/// let _ = model.get_time_end(); +/// +/// let _ = model.get_note_lo(); +/// let _ = model.get_note_axis(); +/// let _ = model.get_note_hi(); +/// ``` +#[derive(Debug, Clone, Default)] pub struct MidiSelection { + pub time_len: Arc, + /// Length of visible time axis + pub time_axis: Arc, + /// Earliest time displayed + pub time_start: Arc, + /// Time step + pub time_zoom: Arc, + /// Auto rezoom to fit in time axis + pub time_lock: Arc, + /// Length of visible note axis + pub note_axis: Arc, + // Lowest note displayed + pub note_lo: Arc, +} diff --git a/app/tek.rs b/app/tek.rs index 828827f1..dc2c3169 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -4,10 +4,18 @@ impl_trait_in_assoc_type, trait_alias, type_alias_impl_trait, type_changing_struct_update )] -mod tek_struct; pub use self::tek_struct::*; -mod tek_trait; pub use self::tek_trait::*; -mod tek_type; pub use self::tek_type::*; -mod tek_impls; +pub mod arrange; +pub mod browse; +pub mod connect; +pub mod device; +pub mod mix; +pub mod plugin; +pub mod sample; +pub mod sequence; +pub mod tick; + +use clap::{self, Parser, Subcommand}; +use builder_pattern::Builder; extern crate xdg; pub(crate) use ::xdg::BaseDirectories; @@ -61,6 +69,7 @@ pub(crate) use tengri::{ ops::{Add, Sub, Mul, Div, Rem}, path::{Path, PathBuf}, sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, + time::Duration, thread::{spawn, JoinHandle}, }, }; @@ -1215,3 +1224,585 @@ pub(crate) const HEADER: &'static str = r#" ~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ █ term█▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~ ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; + +/// Total state +/// +/// ``` +/// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; +/// let mut app = tek::App::default(); +/// let _ = app.scene_add(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)] pub struct App { + /// Base color. + pub color: ItemTheme, + /// Must not be dropped for the duration of the process + pub jack: Jack<'static>, + /// Display size + pub size: Measure, + /// Performance counter + pub perf: PerfModel, + /// Available view modes and input bindings + pub config: Config, + /// Currently selected mode + pub mode: Arc>>, + /// Undo history + pub history: Vec<(AppCommand, Option)>, + /// Dialog overlay + pub dialog: Dialog, + /// Contains all recently created clips. + pub pool: Pool, + /// Contains the currently edited musical arrangement + pub project: Arrangement, + /// Error, if any + pub error: Arc>>> +} + +/// Configuration: mode, view, and bind definitions. +/// +/// ``` +/// let config = tek::Config::default(); +/// ``` +/// +/// ``` +/// // Some dizzle. +/// // What indentation to use here lol? +/// let source = stringify!((mode :menu (name Menu) +/// (info Mode selector.) (keys :axis/y :confirm) +/// (view (bg (g 0) (bsp/s :ports/out +/// (bsp/n :ports/in +/// (bg (g 30) (bsp/s (fixed/y 7 :logo) +/// (fill :dialog/menu))))))))); +/// // Add this definition to the config and try to load it. +/// // A "mode" is basically a state machine +/// // with associated input and output definitions. +/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap(); +/// ``` +#[derive(Default, Debug)] pub struct Config { + /// XDG base directories of running user. + pub dirs: BaseDirectories, + /// Active collection of interaction modes. + pub modes: Modes, + /// Active collection of event bindings. + pub binds: Binds, + /// Active collection of view definitions. + pub views: Views, +} + +/// Group of view and keys definitions. +/// +/// ``` +/// let mode = tek::Mode::>::default(); +/// ``` +#[derive(Default, Debug)] pub struct Mode { + pub path: PathBuf, + pub name: Vec, + pub info: Vec, + pub view: Vec, + pub keys: Vec, + pub modes: Modes, +} + +/// An map of input events (e.g. [TuiEvent]) to [Binding]s. +/// +/// ``` +/// let lang = "(@x (nop)) (@y (nop) (nop))"; +/// let bind = tek::Bind::>::load(&lang).unwrap(); +/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1)); +/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2)); +/// ``` +#[derive(Debug)] pub struct Bind( + /// Map of each event (e.g. key combination) to + /// all command expressions bound to it by + /// all loaded input layers. + pub BTreeMap>> +); + +/// A sequence of zero or more commands (e.g. [AppCommand]), +/// optionally filtered by [Condition] to form layers. +/// +/// ``` +/// //FIXME: Why does it overflow? +/// //let binding: Binding<()> = tek::Binding { ..Default::default() }; +/// ``` +#[derive(Debug, Clone)] pub struct Binding { + pub commands: Arc<[C]>, + pub condition: Option, + pub description: Option>, + pub source: Option>, +} + +/// Condition that must evaluate to true in order to enable an input layer. +/// +/// ``` +/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true}))); +/// ``` +#[derive(Clone)] pub struct Condition( + pub Arcbool + Send + Sync>> +); + +/// List of menu items. +/// +/// ``` +/// let items: tek::MenuItems = Default::default(); +/// ``` +#[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems( + pub Arc<[MenuItem]> +); + +/// An item of a menu. +/// +/// ``` +/// let item: tek::MenuItem = Default::default(); +/// ``` +#[derive(Clone)] pub struct MenuItem( + /// Label + pub Arc, + /// Callback + pub ArcUsually<()> + Send + Sync>> +); + +/// The command-line interface descriptor. +/// +/// ``` +/// let cli: tek::Cli = Default::default(); +/// +/// use clap::CommandFactory; +/// tek::Cli::command().debug_assert(); +/// ``` +#[derive(Parser)] +#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] +#[derive(Debug, Default)] pub struct Cli { + /// Pre-defined configuration modes. + /// + /// TODO: Replace these with scripted configurations. + #[command(subcommand)] pub action: Action, +} + +/// 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 { + /// 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 create + #[arg(short='t', long)] tracks: Option, + /// Scenes to create + #[arg(short='s', long)] scenes: Option, + }, + /// Import media as new session. + Import, + /// Show configuration. + Config, + /// Show version. + Version, +} + +pub type SceneWith<'a, T> = + (usize, &'a Scene, usize, usize, T); + +/// Collection of interaction modes. +pub type Modes = Arc, Arc>>>>>; + +/// Collection of input bindings. +pub type Binds = Arc, Bind>>>>; + +/// Collection of view definitions. +pub type Views = Arc, Arc>>>; + +pub trait HasClipsSize { fn clips_size (&self) -> &Measure; } + +pub trait HasDevices: AsRef> + AsMut> { + fn devices (&self) -> &Vec { self.as_ref() } + fn devices_mut (&mut self) -> &mut Vec { self.as_mut() } +} +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); +} + +/// A control axis. +/// +/// ``` +/// let axis = tek::ControlAxis::X; +/// ``` +#[derive(Debug, Copy, Clone)] pub enum ControlAxis { + X, Y, Z, I +} + +/// Various possible dialog modes. +/// +/// ``` +/// let dialog: tek::Dialog = Default::default(); +/// ``` +#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { + #[default] None, + Help(usize), + Menu(usize, MenuItems), + Device(usize), + Message(Arc), + Browse(BrowseTarget, Arc), + Options, +} + +/// A device that can be plugged into the chain. +/// +/// ``` +/// let device = tek::Device::default(); +/// ``` +#[derive(Debug, Default)] pub enum Device { + #[default] + Bypass, + Mute, + #[cfg(feature = "sampler")] + Sampler(Sampler), + #[cfg(feature = "lv2")] // TODO + Lv2(Lv2), + #[cfg(feature = "vst2")] // TODO + Vst2, + #[cfg(feature = "vst3")] // TODO + Vst3, + #[cfg(feature = "clap")] // TODO + Clap, + #[cfg(feature = "sf2")] // TODO + Sf2, +} + +/// Some sort of wrapper? +pub struct DeviceAudio<'a>(pub &'a mut Device); + + /// Command-line configuration. + #[cfg(feature = "cli")] impl Cli { + pub fn run (&self) -> Usually<()> { + if let Action::Version = self.action { + return Ok(tek_show_version()) + } + + let mut config = Config::new(None); + config.init()?; + + if let Action::Config = self.action { + tek_print_config(&config); + } else if let Action::List = self.action { + todo!("list sessions") + } else if let Action::Resume = self.action { + todo!("resume session") + } else if let Action::New { + name, bpm, tracks, scenes, sync_lead, sync_follow, + midi_from, midi_from_re, midi_to, midi_to_re, + left_from, right_from, left_to, right_to, .. + } = &self.action { + + // Connect to JACK + let name = name.as_ref().map_or("tek", |x|x.as_str()); + let jack = Jack::new(&name)?; + + // TODO: Collect audio IO: + let empty = &[] as &[&str]; + let left_froms = Connect::collect(&left_from, empty, empty); + let left_tos = Connect::collect(&left_to, empty, empty); + let right_froms = Connect::collect(&right_from, empty, empty); + let right_tos = Connect::collect(&right_to, empty, empty); + let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; + let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; + + // Create initial project: + let clock = Clock::new(&jack, *bpm)?; + let mut project = Arrangement::new( + &jack, + None, + clock, + vec![], + vec![], + Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() + .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) + .collect::, _>>()?, + Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() + .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) + .collect::, _>>()? + ); + project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; + project.scenes_add(scenes.unwrap_or(0))?; + + if matches!(self.action, Action::Status) { + // Show status and exit + tek_print_status(&project); + return Ok(()) + } + + // Initialize the app state + let app = tek(&jack, project, config, ":menu"); + if matches!(self.action, Action::Headless) { + // TODO: Headless mode (daemon + client over IPC, then over network...) + println!("todo headless"); + return Ok(()) + } + + // Run the [Tui] and [Jack] threads with the [App] state. + Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ + + // Between jack init and app's first cycle: + + jack.sync_lead(*sync_lead, |mut state|{ + let clock = app.clock(); + clock.playhead.update_from_sample(state.position.frame() as f64); + state.position.bbt = Some(clock.bbt()); + state.position + })?; + + jack.sync_follow(*sync_follow)?; + + // FIXME: They don't work properly. + + Ok(app) + + })?)?; + } + Ok(()) + } + } + + impl Config { + const CONFIG_DIR: &'static str = "tek"; + const CONFIG_SUB: &'static str = "v0"; + const CONFIG: &'static str = "tek.edn"; + const DEFAULTS: &'static str = include_str!("./tek.edn"); + /// Create a new app configuration from a set of XDG base directories, + pub fn new (dirs: Option) -> Self { + let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); + let dirs = dirs.unwrap_or_else(default); + Self { dirs, ..Default::default() } + } + /// Write initial contents of configuration. + pub fn init (&mut self) -> Usually<()> { + self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ + cfgs.add(&dsl)?; + Ok(()) + })?; + Ok(()) + } + /// Write initial contents of a configuration file. + pub fn init_one ( + &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> + ) -> Usually<()> { + if self.dirs.find_config_file(path).is_none() { + //println!("Creating {path:?}"); + std::fs::write(self.dirs.place_config_file(path)?, defaults)?; + } + Ok(if let Some(path) = self.dirs.find_config_file(path) { + //println!("Loading {path:?}"); + let src = std::fs::read_to_string(&path)?; + src.as_str().each(move|item|each(self, item))?; + } else { + return Err(format!("{path}: not found").into()) + }) + } + /// Add statements to configuration from [Dsl] source. + pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { + dsl.each(|item|self.add_one(item))?; + Ok(self) + } + fn add_one (&self, item: impl Language) -> Usually<()> { + if let Some(expr) = item.expr()? { + let head = expr.head()?; + let tail = expr.tail()?; + let name = tail.head()?; + let body = tail.tail()?; + //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); + match head { + Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, + Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, + Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, + _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) + } + Ok(()) + } else { + return Err(format!("Config::load: expected expr, got: {item:?}").into()) + } + } + pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { + self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() + } + } + + impl Mode> { + /// Add a definition to the mode. + /// + /// Supported definitions: + /// + /// - (name ...) -> name + /// - (info ...) -> description + /// - (keys ...) -> key bindings + /// - (mode ...) -> submode + /// - ... -> view + /// + /// ``` + /// let mut mode: tek::Mode> = Default::default(); + /// mode.add("(name hello)").unwrap(); + /// ``` + pub fn add (&mut self, dsl: impl Language) -> Usually<()> { + Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { + //println!("Mode::add: {head} {:?}", expr.tail()); + let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + match head { + "name" => self.add_name(tail)?, + "info" => self.add_info(tail)?, + "keys" => self.add_keys(tail)?, + "mode" => self.add_mode(tail)?, + _ => self.add_view(tail)?, + }; + } else if let Ok(Some(word)) = dsl.word() { + self.add_view(word); + } else { + return Err(format!("Mode::add: unexpected: {dsl:?}").into()); + }) + + //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) + //.word(|word|self.add_view(word)) + //.expr(|expr|expr.head(|head|{ + ////println!("Mode::add: {head} {:?}", expr.tail()); + //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + //match head { + //"name" => self.add_name(tail), + //"info" => self.add_info(tail), + //"keys" => self.add_keys(tail)?, + //"mode" => self.add_mode(tail)?, + //_ => self.add_view(tail), + //}; + //})) + } + + fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.name.push(src.into()))) + } + fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.info.push(src.into()))) + } + fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.view.push(src.into()))) + } + fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) + } + fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(if let Some(id) = dsl.head()? { + load_mode(&self.modes, &id, &dsl.tail())?; + } else { + return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); + })) + } + } + + impl Bind { + /// Create a new event map + pub fn new () -> Self { + Default::default() + } + /// Add a binding to an owned event map. + pub fn def (mut self, event: E, binding: Binding) -> Self { + self.add(event, binding); + self + } + /// Add a binding to an event map. + pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { + if !self.0.contains_key(&event) { + self.0.insert(event.clone(), Default::default()); + } + self.0.get_mut(&event).unwrap().push(binding); + self + } + /// Return the binding(s) that correspond to an event. + pub fn query (&self, event: &E) -> Option<&[Binding]> { + self.0.get(event).map(|x|x.as_slice()) + } + /// Return the first binding that corresponds to an event, considering conditions. + pub fn dispatch (&self, event: &E) -> Option<&Binding> { + self.query(event) + .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) + .flatten() + } + } + + impl Bind> { + pub fn load (lang: &impl Language) -> Usually { + let mut map = Bind::new(); + lang.each(|item|if item.expr().head() == Ok(Some("see")) { + // TODO + Ok(()) + } else if let Ok(Some(_word)) = item.expr().head().word() { + if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { + map.add(key, Binding { + commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), + condition: None, + description: None, + source: None + }); + Ok(()) + } else if Some(":char") == item.expr()?.head()? { + // TODO + return Ok(()) + } else { + return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) + } + } else { + return Err(format!("Config::load_bind: unexpected: {item:?}").into()) + })?; + Ok(map) + } + } diff --git a/app/tek_impls.rs b/app/tek_impls.rs index b6b498cf..3e245cfe 100644 --- a/app/tek_impls.rs +++ b/app/tek_impls.rs @@ -941,55 +941,6 @@ mod clock { }); } -impl Connect { - pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) - -> Vec - { - let mut connections = vec![]; - for port in exact.iter() { connections.push(Self::exact(port)) } - for port in re.iter() { connections.push(Self::regexp(port)) } - for port in re_all.iter() { connections.push(Self::regexp_all(port)) } - connections - } - /// Connect to this exact port - pub fn exact (name: impl AsRef) -> Self { - let info = format!("=:{}", name.as_ref()).into(); - let name = Some(Exact(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp (name: impl AsRef) -> Self { - let info = format!("~:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp_all (name: impl AsRef) -> Self { - let info = format!("+:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn info (&self) -> Arc { - format!(" ({}) {} {}", { - let status = self.status.read().unwrap(); - let mut ok = 0; - for (_, _, state) in status.iter() { - if *state == Connected { - ok += 1 - } - } - format!("{ok}/{}", status.len()) - }, match self.scope { - None => "x", - Some(One) => " ", - Some(All) => "*", - }, match &self.name { - None => format!("x"), - Some(Exact(name)) => format!("= {name}"), - Some(RegExp(name)) => format!("~ {name}"), - }).into() - } -} - - impl Selection { pub fn describe ( &self, @@ -1118,26 +1069,8 @@ impl InteriorMutable for AtomicUsize { fn set (&self, value: usize) -> us impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } impl AsRef> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } } impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.size_inner } } -impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl> RegisterPorts for J { - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiInput::new(self.jack(), name, connect) - } - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiOutput::new(self.jack(), name, connect) - } - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioInput::new(self.jack(), name, connect) - } - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioOutput::new(self.jack(), name, connect) - } -} #[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock); @@ -3562,286 +3495,6 @@ mod pool { mod config { use crate::*; - - /// Command-line configuration. - #[cfg(feature = "cli")] impl Cli { - pub fn run (&self) -> Usually<()> { - if let Action::Version = self.action { - return Ok(tek_show_version()) - } - - let mut config = Config::new(None); - config.init()?; - - if let Action::Config = self.action { - tek_print_config(&config); - } else if let Action::List = self.action { - todo!("list sessions") - } else if let Action::Resume = self.action { - todo!("resume session") - } else if let Action::New { - name, bpm, tracks, scenes, sync_lead, sync_follow, - midi_from, midi_from_re, midi_to, midi_to_re, - left_from, right_from, left_to, right_to, .. - } = &self.action { - - // Connect to JACK - let name = name.as_ref().map_or("tek", |x|x.as_str()); - let jack = Jack::new(&name)?; - - // TODO: Collect audio IO: - let empty = &[] as &[&str]; - let left_froms = Connect::collect(&left_from, empty, empty); - let left_tos = Connect::collect(&left_to, empty, empty); - let right_froms = Connect::collect(&right_from, empty, empty); - let right_tos = Connect::collect(&right_to, empty, empty); - let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; - let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; - - // Create initial project: - let clock = Clock::new(&jack, *bpm)?; - let mut project = Arrangement::new( - &jack, - None, - clock, - vec![], - vec![], - Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() - .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) - .collect::, _>>()?, - Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() - .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) - .collect::, _>>()? - ); - project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; - project.scenes_add(scenes.unwrap_or(0))?; - - if matches!(self.action, Action::Status) { - // Show status and exit - tek_print_status(&project); - return Ok(()) - } - - // Initialize the app state - let app = tek(&jack, project, config, ":menu"); - if matches!(self.action, Action::Headless) { - // TODO: Headless mode (daemon + client over IPC, then over network...) - println!("todo headless"); - return Ok(()) - } - - // Run the [Tui] and [Jack] threads with the [App] state. - Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ - - // Between jack init and app's first cycle: - - jack.sync_lead(*sync_lead, |mut state|{ - let clock = app.clock(); - clock.playhead.update_from_sample(state.position.frame() as f64); - state.position.bbt = Some(clock.bbt()); - state.position - })?; - - jack.sync_follow(*sync_follow)?; - - // FIXME: They don't work properly. - - Ok(app) - - })?)?; - } - Ok(()) - } - } - - impl Config { - const CONFIG_DIR: &'static str = "tek"; - const CONFIG_SUB: &'static str = "v0"; - const CONFIG: &'static str = "tek.edn"; - const DEFAULTS: &'static str = include_str!("./tek.edn"); - /// Create a new app configuration from a set of XDG base directories, - pub fn new (dirs: Option) -> Self { - let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); - let dirs = dirs.unwrap_or_else(default); - Self { dirs, ..Default::default() } - } - /// Write initial contents of configuration. - pub fn init (&mut self) -> Usually<()> { - self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ - cfgs.add(&dsl)?; - Ok(()) - })?; - Ok(()) - } - /// Write initial contents of a configuration file. - pub fn init_one ( - &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> - ) -> Usually<()> { - if self.dirs.find_config_file(path).is_none() { - //println!("Creating {path:?}"); - std::fs::write(self.dirs.place_config_file(path)?, defaults)?; - } - Ok(if let Some(path) = self.dirs.find_config_file(path) { - //println!("Loading {path:?}"); - let src = std::fs::read_to_string(&path)?; - src.as_str().each(move|item|each(self, item))?; - } else { - return Err(format!("{path}: not found").into()) - }) - } - /// Add statements to configuration from [Dsl] source. - pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { - dsl.each(|item|self.add_one(item))?; - Ok(self) - } - fn add_one (&self, item: impl Language) -> Usually<()> { - if let Some(expr) = item.expr()? { - let head = expr.head()?; - let tail = expr.tail()?; - let name = tail.head()?; - let body = tail.tail()?; - //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); - match head { - Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, - Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, - Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, - _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) - } - Ok(()) - } else { - return Err(format!("Config::load: expected expr, got: {item:?}").into()) - } - } - pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { - self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() - } - } - - impl Mode> { - /// Add a definition to the mode. - /// - /// Supported definitions: - /// - /// - (name ...) -> name - /// - (info ...) -> description - /// - (keys ...) -> key bindings - /// - (mode ...) -> submode - /// - ... -> view - /// - /// ``` - /// let mut mode: tek::Mode> = Default::default(); - /// mode.add("(name hello)").unwrap(); - /// ``` - pub fn add (&mut self, dsl: impl Language) -> Usually<()> { - Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { - //println!("Mode::add: {head} {:?}", expr.tail()); - let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - match head { - "name" => self.add_name(tail)?, - "info" => self.add_info(tail)?, - "keys" => self.add_keys(tail)?, - "mode" => self.add_mode(tail)?, - _ => self.add_view(tail)?, - }; - } else if let Ok(Some(word)) = dsl.word() { - self.add_view(word); - } else { - return Err(format!("Mode::add: unexpected: {dsl:?}").into()); - }) - - //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) - //.word(|word|self.add_view(word)) - //.expr(|expr|expr.head(|head|{ - ////println!("Mode::add: {head} {:?}", expr.tail()); - //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - //match head { - //"name" => self.add_name(tail), - //"info" => self.add_info(tail), - //"keys" => self.add_keys(tail)?, - //"mode" => self.add_mode(tail)?, - //_ => self.add_view(tail), - //}; - //})) - } - - fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.name.push(src.into()))) - } - fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.info.push(src.into()))) - } - fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.view.push(src.into()))) - } - fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) - } - fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(if let Some(id) = dsl.head()? { - load_mode(&self.modes, &id, &dsl.tail())?; - } else { - return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); - })) - } - } - - impl Bind { - /// Create a new event map - pub fn new () -> Self { - Default::default() - } - /// Add a binding to an owned event map. - pub fn def (mut self, event: E, binding: Binding) -> Self { - self.add(event, binding); - self - } - /// Add a binding to an event map. - pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { - if !self.0.contains_key(&event) { - self.0.insert(event.clone(), Default::default()); - } - self.0.get_mut(&event).unwrap().push(binding); - self - } - /// Return the binding(s) that correspond to an event. - pub fn query (&self, event: &E) -> Option<&[Binding]> { - self.0.get(event).map(|x|x.as_slice()) - } - /// Return the first binding that corresponds to an event, considering conditions. - pub fn dispatch (&self, event: &E) -> Option<&Binding> { - self.query(event) - .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) - .flatten() - } - } - - impl Bind> { - pub fn load (lang: &impl Language) -> Usually { - let mut map = Bind::new(); - lang.each(|item|if item.expr().head() == Ok(Some("see")) { - // TODO - Ok(()) - } else if let Ok(Some(_word)) = item.expr().head().word() { - if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { - map.add(key, Binding { - commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), - condition: None, - description: None, - source: None - }); - Ok(()) - } else if Some(":char") == item.expr()?.head()? { - // TODO - return Ok(()) - } else { - return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) - } - } else { - return Err(format!("Config::load_bind: unexpected: {item:?}").into()) - })?; - Ok(map) - } - } } mod dialog { diff --git a/app/tek_struct.rs b/app/tek_struct.rs deleted file mode 100644 index 6af03ff8..00000000 --- a/app/tek_struct.rs +++ /dev/null @@ -1,1042 +0,0 @@ -use crate::*; -use clap::{self, Parser, Subcommand}; -use builder_pattern::Builder; - -/// Total state -/// -/// ``` -/// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; -/// let mut app = tek::App::default(); -/// let _ = app.scene_add(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)] pub struct App { - /// Base color. - pub color: ItemTheme, - /// Must not be dropped for the duration of the process - pub jack: Jack<'static>, - /// Display size - pub size: Measure, - /// Performance counter - pub perf: PerfModel, - /// Available view modes and input bindings - pub config: Config, - /// Currently selected mode - pub mode: Arc>>, - /// Undo history - pub history: Vec<(AppCommand, Option)>, - /// Dialog overlay - pub dialog: Dialog, - /// Contains all recently created clips. - pub pool: Pool, - /// Contains the currently edited musical arrangement - pub project: Arrangement, - /// Error, if any - pub error: Arc>>> -} - -/// Configuration: mode, view, and bind definitions. -/// -/// ``` -/// let config = tek::Config::default(); -/// ``` -/// -/// ``` -/// // Some dizzle. -/// // What indentation to use here lol? -/// let source = stringify!((mode :menu (name Menu) -/// (info Mode selector.) (keys :axis/y :confirm) -/// (view (bg (g 0) (bsp/s :ports/out -/// (bsp/n :ports/in -/// (bg (g 30) (bsp/s (fixed/y 7 :logo) -/// (fill :dialog/menu))))))))); -/// // Add this definition to the config and try to load it. -/// // A "mode" is basically a state machine -/// // with associated input and output definitions. -/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap(); -/// ``` -#[derive(Default, Debug)] pub struct Config { - /// XDG base directories of running user. - pub dirs: BaseDirectories, - /// Active collection of interaction modes. - pub modes: Modes, - /// Active collection of event bindings. - pub binds: Binds, - /// Active collection of view definitions. - pub views: Views, -} - -/// Group of view and keys definitions. -/// -/// ``` -/// let mode = tek::Mode::>::default(); -/// ``` -#[derive(Default, Debug)] pub struct Mode { - pub path: PathBuf, - pub name: Vec, - pub info: Vec, - pub view: Vec, - pub keys: Vec, - pub modes: Modes, -} - -/// An map of input events (e.g. [TuiEvent]) to [Binding]s. -/// -/// ``` -/// let lang = "(@x (nop)) (@y (nop) (nop))"; -/// let bind = tek::Bind::>::load(&lang).unwrap(); -/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1)); -/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2)); -/// ``` -#[derive(Debug)] pub struct Bind( - /// Map of each event (e.g. key combination) to - /// all command expressions bound to it by - /// all loaded input layers. - pub BTreeMap>> -); - -/// A sequence of zero or more commands (e.g. [AppCommand]), -/// optionally filtered by [Condition] to form layers. -/// -/// ``` -/// //FIXME: Why does it overflow? -/// //let binding: Binding<()> = tek::Binding { ..Default::default() }; -/// ``` -#[derive(Debug, Clone)] pub struct Binding { - pub commands: Arc<[C]>, - pub condition: Option, - pub description: Option>, - pub source: Option>, -} - -/// Condition that must evaluate to true in order to enable an input layer. -/// -/// ``` -/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true}))); -/// ``` -#[derive(Clone)] pub struct Condition( - pub Arcbool + Send + Sync>> -); - -/// List of menu items. -/// -/// ``` -/// let items: tek::MenuItems = Default::default(); -/// ``` -#[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems( - pub Arc<[MenuItem]> -); - -/// An item of a menu. -/// -/// ``` -/// let item: tek::MenuItem = Default::default(); -/// ``` -#[derive(Clone)] pub struct MenuItem( - /// Label - pub Arc, - /// Callback - pub ArcUsually<()> + Send + Sync>> -); - -/// The command-line interface descriptor. -/// -/// ``` -/// let cli: tek::Cli = Default::default(); -/// -/// use clap::CommandFactory; -/// tek::Cli::command().debug_assert(); -/// ``` -#[derive(Parser)] -#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] -#[derive(Debug, Default)] pub struct Cli { - /// Pre-defined configuration modes. - /// - /// TODO: Replace these with scripted configurations. - #[command(subcommand)] pub action: Action, -} - -/// 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 { - /// 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 create - #[arg(short='t', long)] tracks: Option, - /// Scenes to create - #[arg(short='s', long)] scenes: Option, - }, - /// Import media as new session. - Import, - /// Show configuration. - Config, - /// Show version. - Version, -} - -/// A control axis. -/// -/// ``` -/// let axis = tek::ControlAxis::X; -/// ``` -#[derive(Debug, Copy, Clone)] pub enum ControlAxis { - X, Y, Z, I -} - -/// Various possible dialog modes. -/// -/// ``` -/// let dialog: tek::Dialog = Default::default(); -/// ``` -#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { - #[default] None, - Help(usize), - Menu(usize, MenuItems), - Device(usize), - Message(Arc), - Browse(BrowseTarget, Arc), - Options, -} - -/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) -/// -/// ``` -/// let _ = tek::Timebase::default(); -/// ``` -#[derive(Debug, Clone)] pub struct Timebase { - /// Audio samples per second - pub sr: SampleRate, - /// MIDI beats per minute - pub bpm: Bpm, - /// MIDI ticks per beat - pub ppq: Ppq, -} - -/// Iterator that emits subsequent ticks within a range. -/// -/// ``` -/// let iter = tek::Ticker::default(); -/// ``` -#[derive(Debug, Default)] pub struct Ticker { - pub spp: f64, - pub sample: usize, - pub start: usize, - pub end: usize, -} - -/// -/// ``` -/// let _ = tek::MidiCursor::default(); -/// ``` -#[derive(Debug, Clone)] pub struct MidiCursor { - /// Time coordinate of cursor - pub time_pos: Arc, - /// Note coordinate of cursor - pub note_pos: Arc, - /// Length of note that will be inserted, in pulses - pub note_len: Arc, -} - -/// -/// ``` -/// use tek::{TimeRange, NoteRange}; -/// let model = tek::MidiSelection::from((1, false)); -/// -/// let _ = model.get_time_len(); -/// let _ = model.get_time_zoom(); -/// let _ = model.get_time_lock(); -/// let _ = model.get_time_start(); -/// let _ = model.get_time_axis(); -/// let _ = model.get_time_end(); -/// -/// let _ = model.get_note_lo(); -/// let _ = model.get_note_axis(); -/// let _ = model.get_note_hi(); -/// ``` -#[derive(Debug, Clone, Default)] pub struct MidiSelection { - pub time_len: Arc, - /// Length of visible time axis - pub time_axis: Arc, - /// Earliest time displayed - pub time_start: Arc, - /// Time step - pub time_zoom: Arc, - /// Auto rezoom to fit in time axis - pub time_lock: Arc, - /// Length of visible note axis - pub note_axis: Arc, - // Lowest note displayed - pub note_lo: Arc, -} - -/// A point in time in all time scales (microsecond, sample, MIDI pulse) -/// -/// ``` -/// let _ = tek::Moment::default(); -/// ``` -#[derive(Debug, Default, Clone)] pub struct Moment { - pub timebase: Arc, - /// Current time in microseconds - pub usec: Microsecond, - /// Current time in audio samples - pub sample: SampleCount, - /// Current time in MIDI pulses - pub pulse: Pulse, -} - -/// -/// ``` -/// let _ = tek::Moment2::default(); -/// ``` -#[derive(Debug, Clone, Default)] pub enum Moment2 { - #[default] None, - Zero, - Usec(Microsecond), - Sample(SampleCount), - Pulse(Pulse), -} - -/// MIDI resolution in PPQ (pulses per quarter note) -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64); - -/// Timestamp in MIDI pulses -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64); - -/// Tempo in beats per minute -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64); - -/// Quantization setting for launching clips -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64); - -/// Quantization setting for notes -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64); - -/// Timestamp in audio samples -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64); - -/// Audio sample rate in Hz (samples per second) -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64); - -/// Timestamp in microseconds -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64); - -/// The source of time. -/// -/// ``` -/// let clock = tek::Clock::default(); -/// ``` -#[derive(Clone, Default)] pub struct Clock { - /// JACK transport handle. - pub transport: Arc>, - /// Global temporal resolution (shared by [Moment] fields) - pub timebase: Arc, - /// Current global sample and usec (monotonic from JACK clock) - pub global: Arc, - /// Global sample and usec at which playback started - pub started: Arc>>, - /// Playback offset (when playing not from start) - pub offset: Arc, - /// Current playhead position - pub playhead: Arc, - /// Note quantization factor - pub quant: Arc, - /// Launch quantization factor - pub sync: Arc, - /// Size of buffer in samples - pub chunk: Arc, - // Cache of formatted strings - pub view_cache: Arc>, - /// For syncing the clock to an external source - #[cfg(feature = "port")] pub midi_in: Arc>>, - /// For syncing other devices to this clock - #[cfg(feature = "port")] pub midi_out: Arc>>, - /// For emitting a metronome - #[cfg(feature = "port")] pub click_out: Arc>>, -} - -/// Contains memoized renders of clock values. -/// -/// Performance optimization. -#[derive(Debug)] pub struct ClockView { - pub sr: Memo, String>, - pub buf: Memo, String>, - pub lat: Memo, String>, - pub bpm: Memo, String>, - pub beat: Memo, String>, - pub time: Memo, String>, -} - -/// Arranger. -/// -/// ``` -/// let arranger = tek::Arrangement::default(); -/// ``` -#[derive(Default, Debug)] pub struct Arrangement { - /// Project name. - pub name: Arc, - /// Base color. - pub color: ItemTheme, - /// JACK client handle. - pub jack: Jack<'static>, - /// FIXME a render of the project arrangement, redrawn on update. - /// TODO rename to "render_cache" or smth - pub arranger: Arc>, - /// Display size - pub size: Measure, - /// Display size of clips area - pub size_inner: Measure, - /// 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, - /// Selected UI element - #[cfg(feature = "select")] pub selection: Selection, - /// 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 scene_scroll: usize, -} - -/// 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: Measure, -} - -pub(crate) struct EntriesIterator<'a> { - pub browser: &'a Browse, - pub offset: usize, - pub length: usize, - pub index: usize, -} - -#[derive(Clone, Debug)] pub enum BrowseTarget { - SaveProject, - LoadProject, - ImportSample(Arc>>), - ExportSample(Arc>>), - ImportClip(Arc>>), - ExportClip(Arc>>), -} - -/// A MIDI sequence. -/// -/// ``` -/// let clip = tek::MidiClip::default(); -/// ``` -#[derive(Debug, Clone, Default)] pub struct MidiClip { - pub uuid: uuid::Uuid, - /// Name of clip - pub name: Arc, - /// Temporal resolution in pulses per quarter note - pub ppq: usize, - /// Length of clip in pulses - pub length: usize, - /// Notes in clip - pub notes: MidiData, - /// Whether to loop the clip or play it once - pub looped: bool, - /// Start of loop - pub loop_start: usize, - /// Length of loop - pub loop_length: usize, - /// All notes are displayed with minimum length - pub percussive: bool, - /// Identifying color of clip - pub color: ItemTheme, -} - -/// A device that can be plugged into the chain. -/// -/// ``` -/// let device = tek::Device::default(); -/// ``` -#[derive(Debug, Default)] pub enum Device { - #[default] - Bypass, - Mute, - #[cfg(feature = "sampler")] - Sampler(Sampler), - #[cfg(feature = "lv2")] // TODO - Lv2(Lv2), - #[cfg(feature = "vst2")] // TODO - Vst2, - #[cfg(feature = "vst3")] // TODO - Vst3, - #[cfg(feature = "clap")] // TODO - Clap, - #[cfg(feature = "sf2")] // TODO - Sf2, -} - -/// Some sort of wrapper? -pub struct DeviceAudio<'a>(pub &'a mut Device); - -/// Contains state for viewing and editing a clip. -/// -/// ``` -/// use std::sync::{Arc, RwLock}; -/// let clip = tek::MidiClip::stop_all(); -/// let mut editor = tek::MidiEditor { -/// mode: tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip)))), -/// size: Default::default(), -/// //keys: Default::default(), -/// }; -/// let _ = editor.put_note(true); -/// let _ = editor.put_note(false); -/// let _ = editor.clip_status(); -/// let _ = editor.edit_status(); -/// ``` -pub struct MidiEditor { - /// Size of editor on screen - pub size: Measure, - /// View mode and state of editor - pub mode: PianoHorizontal, -} - -/// A clip, rendered as a horizontal piano roll. -/// -/// ``` -/// let piano = tek::PianoHorizontal::default(); -/// ``` -#[derive(Clone, Default)] pub struct PianoHorizontal { - pub clip: Option>>, - /// Buffer where the whole clip is rerendered on change - pub buffer: Arc>, - /// Size of actual notes area - pub size: Measure, - /// The display window - pub range: MidiSelection, - /// The note cursor - pub point: MidiCursor, - /// The highlight color palette - pub color: ItemTheme, - /// Width of the keyboard - pub keys_width: u16, -} - -/// 12 piano keys, some highlighted. -/// -/// ``` -/// let keys = tek::OctaveVertical::default(); -/// ``` -#[derive(Copy, Clone)] pub struct OctaveVertical { - pub on: [bool; 12], - pub colors: [Color; 3] -} - -/// A LV2 plugin. -#[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 { - /// JACK client handle (needs to not be dropped for standalone mode to work). - pub jack: Jack<'static>, - pub name: Arc, - pub path: Option>, - pub selected: usize, - pub mapping: bool, - pub midi_ins: Vec>, - pub midi_outs: Vec>, - pub audio_ins: Vec>, - pub audio_outs: Vec>, - - pub lv2_world: livi::World, - pub lv2_instance: livi::Instance, - pub lv2_plugin: livi::Plugin, - pub lv2_features: Arc, - pub lv2_port_list: Vec, - pub lv2_input_buffer: Vec, - pub lv2_ui_thread: Option>, -} - -/// A LV2 plugin's X11 UI. -#[cfg(feature = "lv2_gui")] pub struct LV2PluginUI { - pub window: Option -} - -#[derive(Debug, Default)] pub enum MeteringMode { - #[default] Rms, - Log10, -} - -#[derive(Debug, Default, Clone)] pub struct Log10Meter(pub f32); - -#[derive(Debug, Default, Clone)] pub struct RmsMeter(pub f32); - -#[derive(Debug, Default)] pub enum MixingMode { - #[default] Summing, - Average, -} - -/// A clip pool. -/// -/// ``` -/// let pool = tek::Pool::default(); -/// ``` -#[derive(Debug)] pub struct Pool { - pub visible: bool, - /// Selected clip - pub clip: AtomicUsize, - /// Mode switch - pub mode: Option, - /// Embedded file browse - #[cfg(feature = "browse")] pub browse: Option, - /// Collection of MIDI clips. - #[cfg(feature = "clip")] pub clips: Arc>>>>, - /// Collection of sound samples. - #[cfg(feature = "sampler")] pub samples: Arc>>>>, -} - -/// 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, -} - -/// Some sort of wrapper again? -pub struct PoolView<'a>(pub &'a Pool); - -/// Audio input port. -#[derive(Debug)] pub struct AudioInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// Audio output port. -#[derive(Debug)] pub struct AudioOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI input port. -#[derive(Debug)] pub struct MidiInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of currently held notes. - pub held: Arc>, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI output port. -#[derive(Debug)] pub struct MidiOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, - /// List of currently held notes. - pub held: Arc>, - /// Buffer - pub note_buffer: Vec, - /// Buffer - pub output_buffer: Vec>>, -} - -/// Port connection manager. -/// -/// ``` -/// let connect = tek::Connect::default(); -/// ``` -#[derive(Clone, Debug, Default)] pub struct Connect { - pub name: Option, - pub scope: Option, - pub status: Arc, Arc, ConnectStatus)>>>, - pub info: Arc, -} - -/// Plays [Voice]s from [Sample]s. -/// -/// ``` -/// let sampler = tek::Sampler::default(); -/// ``` -#[derive(Debug, Default)] pub struct Sampler { - /// Name of sampler. - pub name: Arc, - /// Device color. - pub color: ItemTheme, - /// Sample currently being recorded. - pub recording: Option<(usize, Option>>)>, - /// Recording buffer. - pub buffer: Vec>, - /// Samples mapped to MIDI notes. - pub samples: SampleKit<128>, - /// Collection of currently playing instances of samples. - pub voices: Arc>>, - /// Samples that are not mapped to MIDI notes. - pub unmapped: Vec>>, - /// Sample currently being edited. - pub editing: Option>>, - /// How to mix the voices. - pub mixing_mode: MixingMode, - /// How to meter the inputs and outputs. - pub metering_mode: MeteringMode, - /// Fixed gain applied to all output. - pub output_gain: f32, - /// Currently active modal, if any. - pub mode: Option, - /// Size of rendered sampler. - pub size: Measure, - /// Lowest note displayed. - pub note_lo: AtomicUsize, - /// Currently selected note. - pub note_pt: AtomicUsize, - /// Selected note as row/col. - pub cursor: (AtomicUsize, AtomicUsize), - /// Audio input meters. - #[cfg(feature = "meter")] pub input_meters: Vec, - /// Audio input ports. Samples are recorded from here. - #[cfg(feature = "port")] pub audio_ins: Vec, - /// MIDI input port. Sampler are triggered from here. - #[cfg(feature = "port")] pub midi_in: Option, - /// Audio output ports. Voices are played into here. - #[cfg(feature = "port")] pub audio_outs: Vec, - /// Audio output meters. - #[cfg(feature = "meter")] pub output_meters: Vec, -} - -/// Collection of samples, one per slot, fixed number of slots. -/// -/// History: Separated to cleanly implement [Default]. -/// -/// ``` -/// let samples = tek::SampleKit([None, None, None, None]); -/// ``` -#[derive(Debug)] pub struct SampleKit( - pub [Option>>;N] -); - -/// A sound cut. -/// -/// ``` -/// let sample = tek::Sample::default(); -/// let sample = tek::Sample::new("test", 0, 0, vec![]); -/// ``` -#[derive(Default, Debug)] pub struct Sample { - pub name: Arc, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, - pub gain: f32, - pub color: ItemTheme, -} - -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - -#[derive(Default, Debug)] pub struct SampleAdd { - pub exited: bool, - pub dir: PathBuf, - pub subdirs: Vec, - pub files: Vec, - pub cursor: usize, - pub offset: usize, - pub sample: Arc>, - pub voices: Arc>>, - pub _search: Option, -} - -/// A scene consists of a set of clips to play together. -/// -/// ``` -/// let scene: tek::Scene = Default::default(); -/// let _ = scene.pulses(); -/// let _ = scene.is_playing(&[]); -/// ``` -#[derive(Debug, Default)] pub struct Scene { - /// Name of scene - pub name: Arc, - /// Identifying color of scene - pub color: ItemTheme, - /// Clips in scene, one per track - pub clips: Vec>>>, -} - -/// Represents the current user selection in the arranger -#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { - #[default] - /// Nothing is selected - Nothing, - /// The whole mix is selected - Mix, - /// A MIDI input is selected. - Input(usize), - /// A MIDI output is selected. - Output(usize), - /// A scene is selected. - #[cfg(feature = "scene")] Scene(usize), - /// A track is selected. - #[cfg(feature = "track")] Track(usize), - /// A clip (track × scene) is selected. - #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, - /// A track's MIDI input connection is selected. - #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, - /// A track's MIDI output connection is selected. - #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, - /// A track device slot is selected. - #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, -} - -/// Contains state for playing a clip -/// -/// ``` -/// let clip = tek::MidiClip::default(); -/// println!("Empty clip: {clip:?}"); -/// -/// let clip = tek::MidiClip::stop_all(); -/// println!("Panic clip: {clip:?}"); -/// -/// let mut clip = tek::MidiClip::new("clip", true, 1, None, None); -/// clip.set_length(96); -/// clip.toggle_loop(); -/// clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); -/// assert!(clip.contains_note_on(36.into(), 6, 18)); -/// assert_eq!(&clip.notes, &clip.duplicate().notes); -/// -/// let clip = std::sync::Arc::new(clip); -/// assert_eq!(clip.clone(), clip); -/// -/// let sequencer = tek::Sequencer::default(); -/// println!("{sequencer:?}"); -/// ``` -pub struct Sequencer { - /// State of clock and playhead - #[cfg(feature = "clock")] pub clock: Clock, - /// Start time and clip being played - #[cfg(feature = "clip")] pub play_clip: Option<(Moment, Option>>)>, - /// Start time and next clip - #[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option>>)>, - /// Record from MIDI ports to current sequence. - #[cfg(feature = "port")] pub midi_ins: Vec, - /// Play from current sequence to MIDI ports - #[cfg(feature = "port")] pub midi_outs: Vec, - /// Play input through output. - pub monitoring: bool, - /// Write input to sequence. - pub recording: bool, - /// Overdub input to sequence. - pub overdub: bool, - /// Send all notes off - pub reset: bool, // TODO?: after Some(nframes) - /// Notes currently held at input - pub notes_in: Arc>, - /// Notes currently held at output - pub notes_out: Arc>, - /// MIDI output buffer - pub note_buf: Vec, - /// MIDI output buffer - pub midi_buf: Vec>>, -} - -/// A track consists of a sequencer and zero or more devices chained after it. -/// -/// ``` -/// let track: tek::Track = Default::default(); -/// ``` -#[derive(Debug, Default)] pub struct Track { - /// Name of track - pub name: Arc, - /// Identifying color of track - pub color: ItemTheme, - /// Preferred width of track column - pub width: usize, - /// MIDI sequencer state - pub sequencer: Sequencer, - /// Device chain - pub devices: Vec, -} - -// Commands supported by [Browse] -//#[derive(Debug, Clone, PartialEq)] -//pub enum BrowseCommand { - //Begin, - //Cancel, - //Confirm, - //Select(usize), - //Chdir(PathBuf), - //Filter(Arc), -//} - -/// 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, -} - -#[derive(Clone, Debug, PartialEq)] pub enum ConnectName { - /** Exact match */ - Exact(Arc), - /** Match regular expression */ - RegExp(Arc), -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { - One, - All -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { - Missing, - Disconnected, - Connected, - Mismatch, -} - -#[derive(Debug)] pub enum SamplerMode { - // Load sample from path - Import(usize, Browse), -} diff --git a/app/tek_trait.rs b/app/tek_trait.rs deleted file mode 100644 index 57fdde33..00000000 --- a/app/tek_trait.rs +++ /dev/null @@ -1,919 +0,0 @@ -use crate::*; -use std::sync::atomic::Ordering; - -pub trait Gettable { - /// Returns current value - fn get (&self) -> T; -} - -pub trait Mutable: Gettable { - /// Sets new value, returns old - fn set (&mut self, value: T) -> T; -} - -pub trait InteriorMutable: Gettable { - /// Sets new value, returns old - fn set (&self, value: T) -> T; -} - -pub trait NotePoint { - fn note_len (&self) -> &AtomicUsize; - /// Get the current length of the note cursor. - fn get_note_len (&self) -> usize { - self.note_len().load(Relaxed) - } - /// Set the length of the note cursor, returning the previous value. - fn set_note_len (&self, x: usize) -> usize { - self.note_len().swap(x, Relaxed) - } - - fn note_pos (&self) -> &AtomicUsize; - /// Get the current pitch of the note cursor. - fn get_note_pos (&self) -> usize { - self.note_pos().load(Relaxed).min(127) - } - /// Set the current pitch fo the note cursor, returning the previous value. - fn set_note_pos (&self, x: usize) -> usize { - self.note_pos().swap(x.min(127), Relaxed) - } -} - -pub trait TimePoint { - fn time_pos (&self) -> &AtomicUsize; - /// Get the current time position of the note cursor. - fn get_time_pos (&self) -> usize { - self.time_pos().load(Relaxed) - } - /// Set the current time position of the note cursor, returning the previous value. - fn set_time_pos (&self, x: usize) -> usize { - self.time_pos().swap(x, Relaxed) - } -} - -pub trait MidiPoint: NotePoint + TimePoint { - /// Get the current end of the note cursor. - fn get_note_end (&self) -> usize { - self.get_time_pos() + self.get_note_len() - } -} - -pub trait TimeRange { - fn time_len (&self) -> &AtomicUsize; - fn get_time_len (&self) -> usize { - self.time_len().load(Ordering::Relaxed) - } - fn time_zoom (&self) -> &AtomicUsize; - fn get_time_zoom (&self) -> usize { - self.time_zoom().load(Ordering::Relaxed) - } - fn set_time_zoom (&self, value: usize) -> usize { - self.time_zoom().swap(value, Ordering::Relaxed) - } - fn time_lock (&self) -> &AtomicBool; - fn get_time_lock (&self) -> bool { - self.time_lock().load(Ordering::Relaxed) - } - fn set_time_lock (&self, value: bool) -> bool { - self.time_lock().swap(value, Ordering::Relaxed) - } - fn time_start (&self) -> &AtomicUsize; - fn get_time_start (&self) -> usize { - self.time_start().load(Ordering::Relaxed) - } - fn set_time_start (&self, value: usize) -> usize { - self.time_start().swap(value, Ordering::Relaxed) - } - fn time_axis (&self) -> &AtomicUsize; - fn get_time_axis (&self) -> usize { - self.time_axis().load(Ordering::Relaxed) - } - fn get_time_end (&self) -> usize { - self.time_start().get() + self.time_axis().get() * self.time_zoom().get() - } -} - -pub trait NoteRange { - fn note_lo (&self) -> &AtomicUsize; - fn get_note_lo (&self) -> usize { - self.note_lo().load(Ordering::Relaxed) - } - fn set_note_lo (&self, x: usize) -> usize { - self.note_lo().swap(x, Ordering::Relaxed) - } - fn note_axis (&self) -> &AtomicUsize; - fn get_note_axis (&self) -> usize { - self.note_axis().load(Ordering::Relaxed) - } - fn get_note_hi (&self) -> usize { - (self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127) - } -} - -pub trait MidiRange: TimeRange + NoteRange {} - -/// A unit of time, represented as an atomic 64-bit float. -/// -/// According to https://stackoverflow.com/a/873367, as per IEEE754, -/// every integer between 1 and 2^53 can be represented exactly. -/// This should mean that, even at 192kHz sampling rate, over 1 year of audio -/// can be clocked in microseconds with f64 without losing precision. -pub trait TimeUnit: InteriorMutable {} - -pub trait HasClipsSize { fn clips_size (&self) -> &Measure; } - -pub trait HasMidiClip { - fn clip (&self) -> Option>>; -} -pub trait HasClock: AsRef + AsMut { - fn clock (&self) -> &Clock { self.as_ref() } - fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } -} -pub trait HasDevices: AsRef> + AsMut> { - fn devices (&self) -> &Vec { self.as_ref() } - fn devices_mut (&mut self) -> &mut Vec { self.as_mut() } -} -pub trait HasSequencer: AsRef + AsMut { - fn sequencer_mut (&mut self) -> &mut Sequencer { self.as_mut() } - fn sequencer (&self) -> &Sequencer { self.as_ref() } -} -pub trait HasSceneScroll: HasScenes { fn scene_scroll (&self) -> usize; } -pub trait HasTrackScroll: HasTracks { fn track_scroll (&self) -> usize; } -pub trait HasScene: AsRefOpt + AsMutOpt { - fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() } - fn scene (&self) -> Option<&Scene> { self.as_ref_opt() } -} -pub trait HasSelection: AsRef + AsMut { - fn selection (&self) -> &Selection { self.as_ref() } - fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } - /// Get the active track - #[cfg(feature = "track")] - fn selected_track (&self) -> Option<&Track> where Self: HasTracks { - let index = self.selection().track()?; - self.tracks().get(index) - } - /// Get a mutable reference to the active track - #[cfg(feature = "track")] - fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { - let index = self.selection().track()?; - self.tracks_mut().get_mut(index) - } - /// Get the active scene - #[cfg(feature = "scene")] - fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { - let index = self.selection().scene()?; - self.scenes().get(index) - } - /// Get a mutable reference to the active scene - #[cfg(feature = "scene")] - fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { - let index = self.selection().scene()?; - self.scenes_mut().get_mut(index) - } - /// Get the active clip - #[cfg(feature = "clip")] - fn selected_clip (&self) -> Option>> where Self: HasScenes + HasTracks { - self.selected_scene()?.clips.get(self.selection().track()?)?.clone() - } -} -pub trait HasScenes: AsRef> + AsMut> { - fn scenes (&self) -> &Vec { self.as_ref() } - fn scenes_mut (&mut self) -> &mut Vec { self.as_mut() } - /// Generate the default name for a new scene - fn scene_default_name (&self) -> Arc { format!("s{:3>}", self.scenes().len() + 1).into() } - fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) } - /// Add multiple scenes - fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks { - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..n { - let _ = self.scene_add(None, Some( - scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() - ))?; - } - Ok(()) - } - /// Add a scene - fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<(usize, &mut Scene)> where Self: HasTracks - { - let scene = Scene { - name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), - clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(ItemTheme::random), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok((index, &mut self.scenes_mut()[index])) - } -} -pub trait HasWidth { - const MIN_WIDTH: usize; - /// 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 HasMidiBuffers { - fn note_buf_mut (&mut self) -> &mut Vec; - fn midi_buf_mut (&mut self) -> &mut Vec>>; -} - -/// ``` -/// use tek::{*, tengri::*}; -/// -/// struct Test(Option); -/// impl_as_ref_opt!(MidiEditor: |self: Test|self.0.as_ref()); -/// impl_as_mut_opt!(MidiEditor: |self: Test|self.0.as_mut()); -/// -/// let mut host = Test(Some(MidiEditor::default())); -/// let _ = host.editor(); -/// let _ = host.editor_mut(); -/// let _ = host.is_editing(); -/// let _ = host.editor_w(); -/// let _ = host.editor_h(); -/// ``` -pub trait HasEditor: AsRefOpt + AsMutOpt { - fn editor (&self) -> Option<&MidiEditor> { self.as_ref_opt() } - fn editor_mut (&mut self) -> Option<&mut MidiEditor> { self.as_mut_opt() } - fn is_editing (&self) -> bool { self.editor().is_some() } - fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) as usize } - fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) as usize } -} -pub trait HasClips { - fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; - fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>; - fn add_clip (&self) -> (usize, Arc>) { - let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None))); - self.clips_mut().push(clip.clone()); - (self.clips().len() - 1, clip) - } -} -/// Trait for thing that may receive MIDI. -pub trait HasMidiIns { - fn midi_ins (&self) -> &Vec; - fn midi_ins_mut (&mut self) -> &mut Vec; - /// Collect MIDI input from app ports (TODO preallocate large buffers) - fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { - self.midi_ins().iter() - .map(|port|port.port().iter(scope) - .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) - .collect::>()) - .collect::>() - } - fn midi_ins_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_ins().iter().enumerate().map(move|(i, input)|{ - let height = 1 + input.connections().len(); - let data = (i, input.port_name(), input.connections(), y, y + height); - y += height; - data - }) - } -} -/// Trait for thing that may output MIDI. -pub trait HasMidiOuts { - fn midi_outs (&self) -> &Vec; - fn midi_outs_mut (&mut self) -> &mut Vec; - fn midi_outs_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_outs().iter().enumerate().map(move|(i, output)|{ - let height = 1 + output.connections().len(); - let data = (i, output.port_name(), output.connections(), y, y + height); - y += height; - data - }) - } - fn midi_outs_emit (&mut self, scope: &ProcessScope) { - for port in self.midi_outs_mut().iter_mut() { - port.buffer_emit(scope) - } - } -} -pub trait HasTracks: AsRef> + AsMut> { - fn tracks (&self) -> &Vec { self.as_ref() } - fn tracks_mut (&mut self) -> &mut Vec { self.as_mut() } - /// Run audio callbacks for every track and every device - fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { - for track in self.tracks_mut().iter_mut() { - if Control::Quit == Audio::process(&mut track.sequencer, client, scope) { - return Control::Quit - } - for device in track.devices.iter_mut() { - if Control::Quit == DeviceAudio(device).process(client, scope) { - return Control::Quit - } - } - } - Control::Continue - } - fn track_longest_name (&self) -> usize { self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) } - /// Stop all playing clips - fn tracks_stop_all (&mut self) { for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } } - /// Stop all playing clips - fn tracks_launch (&mut self, clips: Option>>>>) { - if let Some(clips) = clips { - for (clip, track) in clips.iter().zip(self.tracks_mut()) { track.sequencer.enqueue_next(clip.as_ref()); } - } else { - for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } - } - } - /// Spacing between tracks. - const TRACK_SPACING: usize = 0; -} -pub trait HasTrack: AsRefOpt + AsMutOpt { - fn track (&self) -> Option<&Track> { self.as_ref_opt() } - fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() } - #[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw + 'a { - self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) - } - #[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw + '_ { - self.track().map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) - } - #[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw { - self.track().map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) - } - #[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw { - self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) - } -} - -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>>)>; - - fn pulses_since_start (&self) -> Option { - if let Some((started, Some(_))) = self.play_clip().as_ref() { - let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); - return Some(elapsed) - } - None - } - - fn pulses_since_start_looped (&self) -> Option<(f64, f64)> { - if let Some((started, Some(clip))) = self.play_clip().as_ref() { - let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); - let length = clip.read().unwrap().length.max(1); // prevent div0 on empty clip - let times = (elapsed as usize / length) as f64; - let elapsed = (elapsed as usize % length) as f64; - return Some((times, elapsed)) - } - None - } - - fn enqueue_next (&mut self, clip: Option<&Arc>>) { - *self.next_clip_mut() = Some((self.clock().next_launch_instant(), clip.cloned())); - *self.reset_mut() = true; - } - - fn play_status (&self) -> impl Draw { - let (name, color): (Arc, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() { - let MidiClip { ref name, color, .. } = *clip.read().unwrap(); - (name.clone(), color) - } else { - ("".into(), Tui::g(64).into()) - }; - let time: String = self.pulses_since_start_looped() - .map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time))) - .unwrap_or_else(||String::from(" ")).into(); - field_v(color, "Now:", format!("{} {}", time, name)) - } - - fn next_status (&self) -> impl Draw { - let mut time: Arc = String::from("--.-.--").into(); - let mut name: Arc = String::from("").into(); - let mut color = ItemTheme::G[64]; - let clock = self.clock(); - if let Some((t, Some(clip))) = self.next_clip() { - let clip = clip.read().unwrap(); - name = clip.name.clone(); - color = clip.color.clone(); - time = { - let target = t.pulse.get(); - let current = clock.playhead.pulse.get(); - if target > current { - let remaining = target - current; - format!("-{:>}", clock.timebase.format_beats_1(remaining)) - } else { - String::new() - } - }.into() - } else if let Some((t, Some(clip))) = self.play_clip() { - let clip = clip.read().unwrap(); - if clip.looped { - name = clip.name.clone(); - color = clip.color.clone(); - let target = t.pulse.get() + clip.length as f64; - let current = clock.playhead.pulse.get(); - if target > current { - time = format!("-{:>}", clock.timebase.format_beats_0(target - current)).into() - } - } else { - name = "Stop".to_string().into(); - } - }; - field_v(color, "Next:", format!("{} {}", time, name)) - } -} - -pub trait MidiMonitor: HasMidiIns + HasMidiBuffers { - /// Input note flags. - fn notes_in (&self) -> &Arc>; - /// Current monitoring status. - fn monitoring (&self) -> bool; - /// Mutable monitoring status. - fn monitoring_mut (&mut self) -> &mut bool; - /// Enable or disable monitoring. - fn toggle_monitor (&mut self) { *self.monitoring_mut() = !self.monitoring(); } - /// Perform monitoring. - fn monitor (&mut self, _scope: &ProcessScope) { /* do nothing by default */ } -} - -pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { - fn recording (&self) -> bool; - fn recording_mut (&mut self) -> &mut bool; - fn toggle_record (&mut self) { - *self.recording_mut() = !self.recording(); - } - - fn overdub (&self) -> bool; - fn overdub_mut (&mut self) -> &mut bool; - fn toggle_overdub (&mut self) { - *self.overdub_mut() = !self.overdub(); - } - - fn record_clip ( - &mut self, - scope: &ProcessScope, - started: Moment, - clip: &Option>>, - ) { - if let Some(clip) = clip { - let sample0 = scope.last_frame_time() as usize; - let start = started.sample.get() as usize; - let _recording = self.recording(); - let timebase = self.clock().timebase().clone(); - let quant = self.clock().quant.get(); - let mut clip = clip.write().unwrap(); - let length = clip.length; - for input in self.midi_ins_mut().iter() { - for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) { - if let LiveEvent::Midi { message, .. } = event { - clip.record_event({ - let sample = (sample0 + sample - start) as f64; - let pulse = timebase.samples_to_pulse(sample); - let quantized = (pulse / quant).round() * quant; - quantized as usize % length - }, message); - } - } - } - } - } - - fn record_next (&mut self) { - // TODO switch to next clip and record into it - } -} - -pub trait MidiViewer: Measured + MidiRange + MidiPoint + Debug + Send + Sync { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize); - fn redraw (&self); - fn clip (&self) -> &Option>>; - fn clip_mut (&mut self) -> &mut Option>>; - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.redraw(); - } - /// Make sure cursor is within note range - fn autoscroll (&self) { - let note_pos = self.get_note_pos().min(127); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - if note_pos < note_lo { - self.note_lo().set(note_pos); - } else if note_pos > note_hi { - self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi)); - } - } - /// Make sure time range is within display - fn autozoom (&self) { - if self.time_lock().get() { - let time_len = self.get_time_len(); - let time_axis = self.get_time_axis(); - let time_zoom = self.get_time_zoom(); - loop { - let time_zoom = self.time_zoom().get(); - let time_area = time_axis * time_zoom; - if time_area > time_len { - let next_time_zoom = note_duration_prev(time_zoom); - if next_time_zoom <= 1 { - break - } - let next_time_area = time_axis * next_time_zoom; - if next_time_area >= time_len { - self.time_zoom().set(next_time_zoom); - } else { - break - } - } else if time_area < time_len { - let prev_time_zoom = note_duration_next(time_zoom); - if prev_time_zoom > 384 { - break - } - let prev_time_area = time_axis * prev_time_zoom; - if prev_time_area <= time_len { - self.time_zoom().set(prev_time_zoom); - } else { - break - } - } - } - if time_zoom != self.time_zoom().get() { - self.redraw() - } - } - //while time_len.div_ceil(time_zoom) > time_axis { - //println!("\r{time_len} {time_zoom} {time_axis}"); - //time_zoom = Note::next(time_zoom); - //} - //self.time_zoom().set(time_zoom); - } -} - -pub trait ClipsView: TracksView + ScenesView { - - fn view_scenes_clips <'a> (&'a self) - -> impl Draw + 'a - { - self.clips_size().of(wh_full(above( - wh_full(origin_se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), - Thunk::new(|to: &mut Tui|for ( - track_index, track, _, _ - ) in self.tracks_with_sizes() { - to.place(&w_exact(track.width as u16, - h_full(self.view_track_clips(track_index, track)))) - })))) - } - - fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw + 'a { - Thunk::new(move|to: &mut Tui|for ( - scene_index, scene, .. - ) in self.scenes_with_sizes() { - let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { - let clip = clip.read().unwrap(); - (format!(" ⏹ {}", &clip.name).into(), clip.color) - } else { - (" ⏹ -- ".into(), ItemTheme::G[32]) - }; - let fg = theme.lightest.rgb; - let mut outline = theme.base.rgb; - let bg = if self.selection().track() == Some(track_index) - && self.selection().scene() == Some(scene_index) - { - outline = theme.lighter.rgb; - theme.light.rgb - } else if self.selection().track() == Some(track_index) - || self.selection().scene() == Some(scene_index) - { - outline = theme.darkest.rgb; - theme.base.rgb - } else { - theme.dark.rgb - }; - let w = if self.selection().track() == Some(track_index) - && let Some(editor) = self.editor () - { - (editor.measure_width() as usize).max(24).max(track.width) - } else { - track.width - } as u16; - let y = if self.selection().scene() == Some(scene_index) - && let Some(editor) = self.editor () - { - (editor.measure_height() as usize).max(12) - } else { - Self::H_SCENE as usize - } as u16; - - to.place(&wh_exact(w, y, below( - wh_full(Outer(true, Style::default().fg(outline))), - wh_full(below( - below( - Tui::fg_bg(outline, bg, wh_full("")), - wh_full(origin_nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))), - ), - wh_full(when(self.selection().track() == Some(track_index) - && self.selection().scene() == Some(scene_index) - && self.is_editing(), self.editor()))))))); - }) - } - -} - -pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured { - - fn tracks_width_available (&self) -> u16 { - (self.measure_width() as u16).saturating_sub(40) - } - - /// Iterate over tracks with their corresponding sizes. - fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { - let _editor_width = self.editor().map(|e|e.measure_width()); - let _active_track = self.selection().track(); - let mut x = 0; - self.tracks().iter().enumerate().map_while(move |(index, track)|{ - let width = track.width.max(8); - if x + width < self.clips_size().w() as usize { - let data = (index, track, x, x + width); - x += width + Self::TRACK_SPACING; - Some(data) - } else { - None - } - }) - } - - fn view_track_names (&self, theme: ItemTheme) -> impl Draw { - let track_count = self.tracks().len(); - let scene_count = self.scenes().len(); - let selected = self.selection(); - let button = south( - button_3("t", "rack ", format!("{}{track_count}", selected.track() - .map(|track|format!("{track}/")).unwrap_or_default()), false), - button_3("s", "cene ", format!("{}{scene_count}", selected.scene() - .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); - let button_2 = south( - button_2("T", "+", false), - button_2("S", "+", false)); - view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb, - h_exact(2, Thunk::new(|to: &mut Tui|{ - for (index, track, x1, _x2) in self.tracks_with_sizes() { - to.place(&x_push(x1 as u16, w_exact(track_width(index, track), - Tui::bg(if selected.track() == Some(index) { - track.color.light.rgb - } else { - track.color.base.rgb - }, south(w_full(origin_nw(east( - format!("·t{index:02} "), - Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name)) - ))), ""))) ));}})))) - } - - fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw { - view_track_row_section(theme, - south(w_full(origin_w(button_2("o", "utput", false))), - Thunk::new(|to: &mut Tui|for port in self.midi_outs().iter() { - to.place(&w_full(origin_w(port.port_name()))); - })), - button_2("O", "+", false), - Tui::bg(theme.darker.rgb, origin_w(Thunk::new(|to: &mut Tui|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&w_exact(track_width(index, track), - origin_nw(h_full(iter_south(1, ||track.sequencer.midi_outs.iter(), - |port, index|Tui::fg(Rgb(255, 255, 255), - h_exact(1, Tui::bg(track.color.dark.rgb, w_full(origin_w( - format!("·o{index:02} {}", port.port_name())))))))))));}})))) - } - - fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw { - let mut h = 0u16; - for track in self.tracks().iter() { - h = h.max(track.sequencer.midi_ins.len() as u16); - } - let content = Thunk::new(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&wh_exact(track_width(index, track), h + 1, - origin_nw(south( - Tui::bg(track.color.base.rgb, - w_full(origin_w(east!( - either(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "), - either(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "), - either(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "), - )))), - iter_south(1, ||track.sequencer.midi_ins.iter(), - |port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb, - w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))); - }); - view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), - Tui::bg(theme.darker.rgb, origin_w(content))) - } - -} - -pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync { - /// Default scene height. - const H_SCENE: usize = 2; - /// Default editor height. - const H_EDITOR: usize = 15; - fn h_scenes (&self) -> u16; - fn w_side (&self) -> u16; - fn w_mid (&self) -> u16; - fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> { - let mut y = 0; - self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{ - let height = if self.selection().scene() == Some(s) && self.editor().is_some() { - 8 - } else { - Self::H_SCENE - }; - if y + height <= self.clips_size().h() as usize { - let data = (s, scene, y, y + height); - y += height; - Some(data) - } else { - None - } - }) - } - - fn view_scenes_names (&self) -> impl Draw { - w_exact(20, Thunk::new(|to: &mut Tui|for (index, scene, ..) in self.scenes_with_sizes() { - to.place(&self.view_scene_name(index, scene)); - })) - } - - fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw + 'a { - let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() { - 7 - } else { - Self::H_SCENE as u16 - }; - let bg = if self.selection().scene() == Some(index) { - scene.color.light.rgb - } else { - scene.color.base.rgb - }; - let a = w_full(origin_w(east(format!("·s{index:02} "), - Tui::fg(Tui::g(255), Tui::bold(true, &scene.name))))); - let b = when(self.selection().scene() == Some(index) && self.is_editing(), - wh_full(origin_nw(south( - self.editor().as_ref().map(|e|e.clip_status()), - self.editor().as_ref().map(|e|e.edit_status()))))); - wh_exact(20, h, Tui::bg(bg, origin_nw(south(a, b)))) - } - -} - -/// May create new MIDI input ports. -pub trait AddMidiIn { - fn midi_in_add (&mut self) -> Usually<()>; -} - -/// May create new MIDI output ports. -pub trait AddMidiOut { - fn midi_out_add (&mut self) -> Usually<()>; -} - -pub trait RegisterPorts: HasJack<'static> { - /// Register a MIDI input port. - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register a MIDI output port. - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio input port. - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio output port. - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; -} - -pub trait JackPort: HasJack<'static> { - - type Port: PortSpec + Default; - - type Pair: PortSpec + Default; - - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized; - - fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { - jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) - .map_err(|e|e.into()) - } - - fn port_name (&self) -> &Arc; - - fn connections (&self) -> &[Connect]; - - fn port (&self) -> &Port; - - fn port_mut (&mut self) -> &mut Port; - - fn into_port (self) -> Port where Self: Sized; - - fn close (self) -> Usually<()> where Self: Sized { - let jack = self.jack().clone(); - Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) - } - - fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { - self.with_client(|c|c.ports(re_name, re_type, flags)) - } - - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - - fn port_by_name (&self, name: impl AsRef) -> Option> { - self.with_client(|c|c.port_by_name(name.as_ref())) - } - - fn connect_to_matching <'k> (&'k self) -> Usually<()> { - for connect in self.connections().iter() { - match &connect.name { - Some(Exact(name)) => { - *connect.status.write().unwrap() = self.connect_exact(name)?; - }, - Some(RegExp(re)) => { - *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; - }, - _ => {}, - }; - } - Ok(()) - } - - fn connect_exact <'k> (&'k self, name: &str) -> - Usually, Arc, ConnectStatus)>> - { - self.with_client(move|c|{ - let mut status = vec![]; - for port in c.ports(None, None, PortFlags::empty()).iter() { - if port.as_str() == &*name { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected { - break - } - } - } - } - Ok(status) - }) - } - - fn connect_regexp <'k> ( - &'k self, re: &str, scope: Option - ) -> Usually, Arc, ConnectStatus)>> { - self.with_client(move|c|{ - let mut status = vec![]; - let ports = c.ports(Some(&re), None, PortFlags::empty()); - for port in ports.iter() { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected && scope == Some(One) { - break - } - } - } - Ok(status) - }) - } - - /** Connect to a matching port by name. */ - fn connect_to_name (&self, name: impl AsRef) -> Usually { - self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { - self.connect_to_unowned(port) - } else { - Ok(Missing) - }) - } - - /** Connect to a matching port by reference. */ - fn connect_to_unowned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } - - /** Connect to an owned matching port by reference. */ - fn connect_to_owned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } - -} diff --git a/app/tek_type.rs b/app/tek_type.rs deleted file mode 100644 index fd2c7d9d..00000000 --- a/app/tek_type.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::*; - -pub type MidiData = - Vec>; - -pub type ClipPool = - Vec>>; - -pub type CollectedMidiInput<'a> = - Vec, MidiError>)>>; - -pub type SceneWith<'a, T> = - (usize, &'a Scene, usize, usize, T); - -pub type MidiSample = - (Option, Arc>); - -/// Collection of interaction modes. -pub type Modes = Arc, Arc>>>>>; - -/// Collection of input bindings. -pub type Binds = Arc, Bind>>>>; - -/// Collection of view definitions. -pub type Views = Arc, Arc>>>; diff --git a/app/tick.rs b/app/tick.rs new file mode 100644 index 00000000..c6bdd169 --- /dev/null +++ b/app/tick.rs @@ -0,0 +1,168 @@ +pub trait HasClock: AsRef + AsMut { + fn clock (&self) -> &Clock { self.as_ref() } + fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } +} + + +/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) +/// +/// ``` +/// let _ = tek::Timebase::default(); +/// ``` +#[derive(Debug, Clone)] pub struct Timebase { + /// Audio samples per second + pub sr: SampleRate, + /// MIDI beats per minute + pub bpm: Bpm, + /// MIDI ticks per beat + pub ppq: Ppq, +} + +/// Iterator that emits subsequent ticks within a range. +/// +/// ``` +/// let iter = tek::Ticker::default(); +/// ``` +#[derive(Debug, Default)] pub struct Ticker { + pub spp: f64, + pub sample: usize, + pub start: usize, + pub end: usize, +} + +/// A point in time in all time scales (microsecond, sample, MIDI pulse) +/// +/// ``` +/// let _ = tek::Moment::default(); +/// ``` +#[derive(Debug, Default, Clone)] pub struct Moment { + pub timebase: Arc, + /// Current time in microseconds + pub usec: Microsecond, + /// Current time in audio samples + pub sample: SampleCount, + /// Current time in MIDI pulses + pub pulse: Pulse, +} + +/// +/// ``` +/// let _ = tek::Moment2::default(); +/// ``` +#[derive(Debug, Clone, Default)] pub enum Moment2 { + #[default] None, + Zero, + Usec(Microsecond), + Sample(SampleCount), + Pulse(Pulse), +} + +/// MIDI resolution in PPQ (pulses per quarter note) +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64); + +/// Timestamp in MIDI pulses +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64); + +/// Tempo in beats per minute +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64); + +/// Quantization setting for launching clips +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64); + +/// Quantization setting for notes +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64); + +/// Timestamp in audio samples +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64); + +/// Audio sample rate in Hz (samples per second) +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64); + +/// Timestamp in microseconds +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64); + +/// The source of time. +/// +/// ``` +/// let clock = tek::Clock::default(); +/// ``` +#[derive(Clone, Default)] pub struct Clock { + /// JACK transport handle. + pub transport: Arc>, + /// Global temporal resolution (shared by [Moment] fields) + pub timebase: Arc, + /// Current global sample and usec (monotonic from JACK clock) + pub global: Arc, + /// Global sample and usec at which playback started + pub started: Arc>>, + /// Playback offset (when playing not from start) + pub offset: Arc, + /// Current playhead position + pub playhead: Arc, + /// Note quantization factor + pub quant: Arc, + /// Launch quantization factor + pub sync: Arc, + /// Size of buffer in samples + pub chunk: Arc, + // Cache of formatted strings + pub view_cache: Arc>, + /// For syncing the clock to an external source + #[cfg(feature = "port")] pub midi_in: Arc>>, + /// For syncing other devices to this clock + #[cfg(feature = "port")] pub midi_out: Arc>>, + /// For emitting a metronome + #[cfg(feature = "port")] pub click_out: Arc>>, +} + +/// A unit of time, represented as an atomic 64-bit float. +/// +/// According to https://stackoverflow.com/a/873367, as per IEEE754, +/// every integer between 1 and 2^53 can be represented exactly. +/// This should mean that, even at 192kHz sampling rate, over 1 year of audio +/// can be clocked in microseconds with f64 without losing precision. +pub trait TimeUnit: InteriorMutable {} + +/// Contains memoized renders of clock values. +/// +/// Performance optimization. +#[derive(Debug)] pub struct ClockView { + pub sr: Memo, String>, + pub buf: Memo, String>, + pub lat: Memo, String>, + pub bpm: Memo, String>, + pub beat: Memo, String>, + pub time: Memo, String>, +}