diff --git a/Cargo.toml b/Cargo.toml index f4e2a106..1a05aaf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,6 @@ proptest-derive = { version = "^0.5.1" } [features] default = ["cli", "arranger", "sampler"] - arranger = ["port", "editor", "sequencer", "track", "scene", "clip", "select"] browse = [] clap = [] diff --git a/Justfile b/Justfile index ebb68b1c..21326baa 100644 --- a/Justfile +++ b/Justfile @@ -2,7 +2,7 @@ export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg export RUST_BACKTRACE := "1" default +ARGS="new": - target/debug/tek {{ARGS}} + cargo run -- {{ARGS}} doc +ARGS="": cargo doc --open -j4 --document-private-items {{ARGS}} diff --git a/app/tek.rs b/app/tek.rs index 178070da..6b2d9963 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -59,7 +59,6 @@ pub(crate) use tengri::{ pub(crate) use ConnectName::*; pub(crate) use ConnectScope::*; pub(crate) use ConnectStatus::*; -pub(crate) use JackState::*; /// Command-line entrypoint. #[cfg(feature = "cli")] pub fn main () -> Usually<()> { @@ -70,7 +69,7 @@ pub(crate) use JackState::*; /// Create a new application from a backend, project, config, and mode /// /// ``` -/// let jack = tek::Jack::new(&"test_tek").expect("failed to connect to jack"); +/// let jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack"); /// let proj = tek::Arrangement::default(); /// let mut conf = tek::Config::default(); /// conf.add("(mode hello)"); diff --git a/app/tek_impls.rs b/app/tek_impls.rs index 53ec57cc..71cca19c 100644 --- a/app/tek_impls.rs +++ b/app/tek_impls.rs @@ -1,9 +1,35 @@ use crate::*; use std::fmt::Write; -use std::path::PathBuf; + +impl +AsMut> HasClock for T {} +impl +AsMut> HasSelection for T {} +impl +AsMut> HasSequencer for T {} +impl >+AsMut>> HasScenes for T {} +impl >+AsMut>> HasTracks for T {} +impl +AsMutOpt> HasEditor for T {} +impl +AsMutOpt+Send+Sync> HasScene for T {} +impl +AsMutOpt+Send+Sync> HasTrack for T {} +impl MidiPoint for T {} +impl > TracksView for T {} +impl MidiRange for T {} +impl ClipsView for T {} mod app { use crate::*; + impl_has!(Clock: |self: App|self.project.clock); + impl_has!(Vec: |self: App|self.project.midi_ins); + impl_has!(Vec: |self: App|self.project.midi_outs); + impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); + impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); + impl_has!(Dialog: |self: App|self.dialog); + impl_has!(Jack<'static>: |self: App|self.jack); + impl_has!(Measure: |self: App|self.size); + impl_has!(Pool: |self: App|self.pool); + impl_has!(Selection: |self: App|self.project.selection); + has_clips!( |self: App|self.pool.clips); + impl_audio!(App: tek_jack_process, tek_jack_event); + impl_as_ref!(Vec: |self: App| self.project.as_ref()); + impl_as_mut!(Vec: |self: App| self.project.as_mut()); impl Draw for App { fn draw (&self, to: &mut TuiOut) { @@ -280,8 +306,6 @@ mod app { } } } - - impl_audio!(App: tek_jack_process, tek_jack_event); } #[cfg(feature = "vst2")] impl ::vst::host::Host for Plugin {} @@ -289,19 +313,20 @@ mod app { mod arrange { use crate::*; - impl_has!(Jack<'static>: |self: Arrangement|self.jack); - impl_has!(Measure: |self: Arrangement|self.size); - - #[cfg(feature = "editor")] impl_has!(Option: |self: Arrangement|self.editor); - #[cfg(feature = "port")] impl_has!(Vec: |self: Arrangement|self.midi_ins); - #[cfg(feature = "port")] impl_has!(Vec: |self: Arrangement|self.midi_outs); - #[cfg(feature = "clock")] impl_has!(Clock: |self: Arrangement|self.clock); - #[cfg(feature = "select")] impl_has!(Selection: |self: Arrangement|self.selection); + impl_has!(Jack<'static>: |self: Arrangement| self.jack); + impl_has!(Measure: |self: Arrangement| self.size); + impl_has!(Vec: |self: Arrangement| self.tracks); + impl_has!(Vec: |self: Arrangement| self.scenes); + #[cfg(feature = "port")] impl_has!(Vec: |self: Arrangement| self.midi_ins); + #[cfg(feature = "port")] impl_has!(Vec: |self: Arrangement| self.midi_outs); + #[cfg(feature = "clock")] impl_has!(Clock: |self: Arrangement| self.clock); + #[cfg(feature = "select")] impl_has!(Selection: |self: Arrangement| self.selection); + #[cfg(feature = "select")] impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); + #[cfg(feature = "select")] impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); + #[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); + #[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); #[cfg(feature = "select")] impl Arrangement { - #[cfg(feature = "clip")] fn selected_clip (&self) -> Option { todo!() } - #[cfg(feature = "scene")] fn selected_scene (&self) -> Option { todo!() } - #[cfg(feature = "track")] fn selected_track (&self) -> Option { todo!() } #[cfg(feature = "port")] fn selected_midi_in (&self) -> Option { todo!() } #[cfg(feature = "port")] fn selected_midi_out (&self) -> Option { todo!() } fn selected_device (&self) -> Option { todo!() } @@ -349,16 +374,6 @@ mod arrange { 2 //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } - /// Get the active track - #[cfg(feature = "track")] pub fn get_track (&self) -> Option<&Track> { - let index = self.selection().track()?; - Has::>::get(self).get(index) - } - /// Get a mutable reference to the active track - #[cfg(feature = "track")] pub fn get_track_mut (&mut self) -> Option<&mut Track> { - let index = self.selection().track()?; - Has::>::get_mut(self).get_mut(index) - } /// Add multiple tracks #[cfg(feature = "track")] pub fn tracks_add ( &mut self, @@ -501,20 +516,6 @@ mod arrange { Align::nw(format!(" · {}", "--"))))))))); })) } - /// Get the active scene - #[cfg(feature = "scene")] pub fn get_scene (&self) -> Option<&Scene> { - let index = self.selection().scene()?; - Has::>::get(self).get(index) - } - /// Get a mutable reference to the active scene - #[cfg(feature = "scene")] pub fn get_scene_mut (&mut self) -> Option<&mut Scene> { - let index = self.selection().scene()?; - Has::>::get_mut(self).get_mut(index) - } - /// Get the active clip - #[cfg(feature = "clip")] pub fn get_clip (&self) -> Option>> { - self.get_scene()?.clips.get(self.selection().track()?)?.clone() - } /// Put a clip in a slot #[cfg(feature = "clip")] pub fn clip_put ( &mut self, track: usize, scene: usize, clip: Option>> @@ -537,17 +538,17 @@ mod arrange { } /// Toggle looping for the active clip #[cfg(feature = "clip")] pub fn toggle_loop (&mut self) { - if let Some(clip) = self.get_clip() { + if let Some(clip) = self.selected_clip() { clip.write().unwrap().toggle_loop() } } /// Get the first sampler of the active track #[cfg(feature = "sampler")] pub fn sampler (&self) -> Option<&Sampler> { - self.get_track()?.sampler(0) + self.selected_track()?.sampler(0) } /// Get the first sampler of the active track #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.get_track_mut()?.sampler_mut(0) + self.selected_track_mut()?.sampler_mut(0) } } @@ -672,7 +673,6 @@ mod browse { //take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -impl + AsMut> HasClock for T {} impl std::fmt::Debug for Clock { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { @@ -950,462 +950,6 @@ impl ClockView { //} } - -impl>> HasEditor for T {} - -impl_has!(Measure: |self: MidiEditor|self.size); - - -impl std::fmt::Debug for MidiEditor { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("MidiEditor").field("mode", &self.mode).finish() - } -} - -from!(MidiEditor: |clip: &Arc>| { - let model = Self::from(Some(clip.clone())); - model.redraw(); - model -}); - -from!(MidiEditor: |clip: Option>>| { - let mut model = Self::default(); - *model.clip_mut() = clip; - model.redraw(); - model -}); - -impl MidiEditor { - /// Put note at current position - pub fn put_note (&mut self, advance: bool) { - let mut redraw = false; - if let Some(clip) = self.clip() { - let mut clip = clip.write().unwrap(); - let note_start = self.get_time_pos(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let note_end = note_start + (note_len.saturating_sub(1)); - let key: u7 = u7::from(note_pos as u8); - let vel: u7 = 100.into(); - let length = clip.length; - let note_end = note_end % length; - let note_on = MidiMessage::NoteOn { key, vel }; - if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { - clip.notes[note_start].push(note_on); - } - let note_off = MidiMessage::NoteOff { key, vel }; - if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { - clip.notes[note_end].push(note_off); - } - if advance { - self.set_time_pos((note_end + 1) % clip.length); - } - redraw = true; - } - if redraw { - self.mode.redraw(); - } - } - fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } - fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } - fn note_length (&self) -> usize { self.get_note_len() } - fn note_pos (&self) -> usize { self.get_note_pos() } - fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } - fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } - fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } - fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } - fn note_len (&self) -> usize { self.get_note_len() } - fn note_len_next (&self) -> usize { self.get_note_len() + 1 } - fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } - fn note_range (&self) -> usize { self.get_note_axis() } - fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } - fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } - fn time_zoom (&self) -> usize { self.get_time_zoom() } - fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_lock (&self) -> bool { self.get_time_lock() } - fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } - fn time_pos (&self) -> usize { self.get_time_pos() } - fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } - fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } - fn time_pos_prev (&self) -> usize { - let step = self.get_note_len(); - self.get_time_pos().overflowing_sub(step) - .0.min(self.clip_length().saturating_sub(step)) - } - fn time_pos_prev_fine (&self) -> usize { - self.get_time_pos().overflowing_sub(1) - .0.min(self.clip_length().saturating_sub(1)) - } - pub fn clip_status (&self) -> impl Content + '_ { - let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.name.clone(), clip.length, clip.looped) - } else { (ItemTheme::G[64], String::new().into(), 0, false) }; - Fixed::X(20, col!( - Fill::X(Align::w(Bsp::e( - button_2("f2", "name ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), - Fill::X(Align::w(Bsp::e( - button_2("l", "ength ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), - Fill::X(Align::w(Bsp::e( - button_2("r", "epeat ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), - )) - } - pub fn edit_status (&self) -> impl Content + '_ { - let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.length) - } else { (ItemTheme::G[64], 0) }; - let time_pos = self.get_time_pos(); - let time_zoom = self.get_time_zoom(); - let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; - let note_pos = self.get_note_pos(); - let note_name = format!("{:4}", note_pitch_to_name(note_pos)); - let note_pos = format!("{:>3}", note_pos); - let note_len = format!("{:>4}", self.get_note_len()); - Fixed::X(20, col!( - Fill::X(Align::w(Bsp::e( - button_2("t", "ime ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), - format!("{length} /{time_zoom} +{time_pos} "))))))), - Fill::X(Align::w(Bsp::e( - button_2("z", "lock ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), - format!("{time_lock}"))))))), - Fill::X(Align::w(Bsp::e( - button_2("x", "note ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), - format!("{note_name} {note_pos} {note_len}"))))))), - )) - } -} - -impl TimeRange for MidiEditor { - fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } -} - -impl NoteRange for MidiEditor { - fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } -} - -impl NotePoint for MidiEditor { - fn note_len (&self) -> &AtomicUsize { self.mode.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } -} - -impl TimePoint for MidiEditor { - fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } -} - -impl MidiViewer for MidiEditor { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } - fn redraw (&self) { self.mode.redraw() } - fn clip (&self) -> &Option>> { self.mode.clip() } - fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } - fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } -} - -impl Layout for MidiEditor { - fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } -} - -impl HasContent for MidiEditor { - fn content (&self) -> impl Content { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } -} - -impl_has!(Measure:|self:PianoHorizontal|self.size); - -impl PianoHorizontal { - pub fn new (clip: Option<&Arc>>) -> Self { - let size = Measure::new(0, 0); - let mut range = MidiSelection::from((12, true)); - range.time_axis = size.x.clone(); - range.note_axis = size.y.clone(); - let piano = Self { - keys_width: 5, - size, - range, - buffer: RwLock::new(Default::default()).into(), - point: MidiCursor::default(), - clip: clip.cloned(), - color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), - }; - piano.redraw(); - piano - } -} - -impl Layout for PianoHorizontal { - fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } -} -impl HasContent for PianoHorizontal { - fn content (&self) -> impl Content { - Bsp::s( - Bsp::e(Fixed::X(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), - Bsp::e(self.keys(), self.size.of(Bsp::b(Fill::XY(self.notes()), Fill::XY(self.cursor())))), - ) - } -} - -impl PianoHorizontal { - /// Draw the piano roll background. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { - for (y, note) in (0..=127).rev().enumerate() { - for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { - let cell = buf.get_mut(x, y).unwrap(); - if note == (127-note_point) || time == time_point { - cell.set_bg(Rgb(0,0,0)); - } else { - cell.set_bg(clip.color.darkest.rgb); - } - if time % 384 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('│'); - } else if time % 96 == 0 { - cell.set_fg(clip.color.dark.rgb); - cell.set_char('╎'); - } else if time % note_len == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('┊'); - } else if (127 - note) % 12 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('='); - } else if (127 - note) % 6 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('—'); - } else { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('·'); - } - } - } - } - /// Draw the piano roll foreground. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { - let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); - let mut notes_on = [false;128]; - for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { - for (_y, note) in (0..=127).rev().enumerate() { - if let Some(cell) = buf.get_mut(x, note) { - if notes_on[note] { - cell.set_char('▂'); - cell.set_style(style); - } - } - } - let time_end = time_start + zoom; - for time in time_start..time_end.min(clip.length) { - for event in clip.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - if let Some(cell) = buf.get_mut(x, note) { - cell.set_char('█'); - cell.set_style(style); - } - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } - } - } - - } - } - fn notes (&self) -> impl Content { - let time_start = self.get_time_start(); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - let buffer = self.buffer.clone(); - Thunk::new(move|to: &mut TuiOut|{ - let source = buffer.read().unwrap(); - let XYWH(x0, y0, w, _h) = to.area(); - //if h as usize != note_axis { - //panic!("area height mismatch: {h} <> {note_axis}"); - //} - for (area_x, screen_x) in (x0..x0+w).enumerate() { - for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { - let source_x = time_start + area_x; - let source_y = note_hi - area_y; - // TODO: enable loop rollover: - //let source_x = (time_start + area_x) % source.width.max(1); - //let source_y = (note_hi - area_y) % source.height.max(1); - let is_in_x = source_x < source.width; - let is_in_y = source_y < source.height; - if is_in_x && is_in_y { - if let Some(source_cell) = source.get(source_x, source_y) { - if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { - *cell = source_cell.clone(); - } - } - } - } - } - }) - } - fn cursor (&self) -> impl Content { - let note_hi = self.get_note_hi(); - let note_lo = self.get_note_lo(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let time_pos = self.get_time_pos(); - let time_start = self.get_time_start(); - let time_zoom = self.get_time_zoom(); - let style = Some(Style::default().fg(self.color.lightest.rgb)); - Thunk::new(move|to: &mut TuiOut|{ - let XYWH(x0, y0, w, _) = to.area(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - if note == note_pos { - for x in 0..w { - let screen_x = x0 + x; - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_pos && time_pos < time_2 { - to.blit(&"█", screen_x, screen_y, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (screen_x + 1)..(screen_x + tail) { - to.blit(&"▂", x_tail, screen_y, style); - } - break - } - } - break - } - } - }) - } - fn keys (&self) -> impl Content { - let state = self; - let color = state.color; - let note_lo = state.get_note_lo(); - let note_hi = state.get_note_hi(); - let note_pos = state.get_note_pos(); - let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); - let off_style = Some(Style::default().fg(Tui::g(255))); - let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); - Fill::Y(Fixed::X(self.keys_width, Thunk::new(move|to: &mut TuiOut|{ - let XYWH(x, y0, _w, _h) = to.area(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - to.blit(&to_key(note), x, screen_y, key_style); - if note > 127 { - continue - } - if note == note_pos { - to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style) - } else { - to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) - }; - } - }))) - } - fn timeline (&self) -> impl Content + '_ { - Fill::X(Fixed::Y(1, Thunk::new(move|to: &mut TuiOut|{ - let XYWH(x, y, w, _h) = to.area(); - let style = Some(Style::default().dim()); - let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { - let t = area_x as usize * self.time_zoom().get(); - if t < length { - to.blit(&"|", screen_x, y, style); - } - } - }))) - } -} - -impl TimeRange for PianoHorizontal { - fn time_len (&self) -> &AtomicUsize { self.range.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.range.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } -} - -impl NoteRange for PianoHorizontal { - fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } -} - -impl NotePoint for PianoHorizontal { - fn note_len (&self) -> &AtomicUsize { self.point.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } -} - -impl TimePoint for PianoHorizontal { - fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } -} - -impl MidiViewer for PianoHorizontal { - fn clip (&self) -> &Option>> { &self.clip } - fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } - /// Determine the required space to render the clip. - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { (clip.length / self.range.time_zoom().get(), 128) } - fn redraw (&self) { - *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { - let clip = clip.read().unwrap(); - let buf_size = self.buffer_size(&clip); - let mut buffer = BigBuffer::from(buf_size); - let time_zoom = self.get_time_zoom(); - self.time_len().set(clip.length); - PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); - PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); - buffer - } else { - Default::default() - } - } - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); - self.redraw(); - } -} - -impl std::fmt::Debug for PianoHorizontal { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - let buffer = self.buffer.read().unwrap(); - f.debug_struct("PianoHorizontal") - .field("time_zoom", &self.range.time_zoom) - .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) - .finish() - } -} -impl OctaveVertical { - fn color (&self, pitch: usize) -> Color { - let pitch = pitch % 12; - self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] - } -} -impl HasContent for OctaveVertical { - fn content (&self) -> impl Content { - row!( - Tui::fg_bg(self.color(0), self.color(1), "▙"), - Tui::fg_bg(self.color(2), self.color(3), "▙"), - Tui::fg_bg(self.color(4), self.color(5), "▌"), - Tui::fg_bg(self.color(6), self.color(7), "▟"), - Tui::fg_bg(self.color(8), self.color(9), "▟"), - Tui::fg_bg(self.color(10), self.color(11), "▟"), - ) - } -} -impl Layout for RmsMeter {} -impl Layout for Log10Meter {} - impl Connect { pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) -> Vec @@ -1555,7 +1099,6 @@ impl Selection { } -impl + AsMut> HasSelection for T {} /// Default is always empty map regardless if `E` and `C` implement [Default]. impl Default for Bind { @@ -1628,9 +1171,7 @@ 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 ClipsView for T {} 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 } } @@ -1652,23 +1193,7 @@ impl> RegisterPorts for J { } } -#[cfg(feature = "clock")] impl_has!(Clock: |self: App|self.project.clock); #[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock); -#[cfg(feature = "clock")] impl_has!(Clock: |self: Sequencer|self.clock); - -#[cfg(feature = "port")] impl_has!(Vec: |self: App|self.project.midi_ins); -#[cfg(feature = "port")] impl_has!(Vec: |self: App|self.project.midi_outs); -#[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_ins); -#[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_outs); - -impl_has!(Jack<'static>: |self: App|self.jack); -impl_has!(Dialog: |self: App|self.dialog); -impl_has!(Measure: |self: App|self.size); -impl_has!(Option: |self: App|self.project.editor); -impl_has!(Pool: |self: App|self.pool); -impl_has!(Selection: |self: App|self.project.selection); -impl_has!(Sequencer: |self: Track|self.sequencer); -has_clips!( |self: App|self.pool.clips); impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); impl_debug!(Condition |self, w| { write!(w, "*") }); @@ -2015,6 +1540,15 @@ mod time { mod midi { use crate::*; + impl_from!(MidiSelection: |data:(usize, bool)| Self { + time_len: Arc::new(0.into()), + note_axis: Arc::new(0.into()), + note_lo: Arc::new(0.into()), + time_axis: Arc::new(0.into()), + time_start: Arc::new(0.into()), + time_zoom: Arc::new(data.0.into()), + time_lock: Arc::new(data.1.into()), + }); impl NotePoint for MidiCursor { fn note_len (&self) -> &AtomicUsize { @@ -2031,20 +1565,6 @@ mod midi { } } - impl MidiPoint for T {} - - from!(MidiSelection: |data:(usize, bool)| Self { - time_len: Arc::new(0.into()), - note_axis: Arc::new(0.into()), - note_lo: Arc::new(0.into()), - time_axis: Arc::new(0.into()), - time_start: Arc::new(0.into()), - time_zoom: Arc::new(data.0.into()), - time_lock: Arc::new(data.1.into()), - }); - - impl MidiRange for T {} - impl TimeRange for MidiSelection { fn time_len (&self) -> &AtomicUsize { &self.time_len } fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } @@ -2196,13 +1716,13 @@ mod midi { parse_midi_input(self.port().iter(scope)) } } - impl>> HasMidiIns for T { - fn midi_ins (&self) -> &Vec { self.get() } - fn midi_ins_mut (&mut self) -> &mut Vec { self.get_mut() } + impl> + AsMut>> HasMidiIns for T { + fn midi_ins (&self) -> &Vec { self.as_ref() } + fn midi_ins_mut (&mut self) -> &mut Vec { self.as_mut() } } - impl>> HasMidiOuts for T { - fn midi_outs (&self) -> &Vec { self.get() } - fn midi_outs_mut (&mut self) -> &mut Vec { self.get_mut() } + impl> + AsMut>> HasMidiOuts for T { + fn midi_outs (&self) -> &Vec { self.as_ref() } + fn midi_outs_mut (&mut self) -> &mut Vec { self.as_mut() } } impl> AddMidiIn for T { fn midi_in_add (&mut self) -> Usually<()> { @@ -2282,7 +1802,7 @@ mod midi { controller: 123.into(), value: 0.into() }]]), - Some(ItemColor::from_rgb(Color::Rgb(32, 32, 32)).into()) + Some(ItemColor::from_tui(Color::Rgb(32, 32, 32)).into()) ) } } @@ -2375,6 +1895,9 @@ mod audio { mod meter { use crate::*; + impl Layout for RmsMeter {} + impl Layout for Log10Meter {} + impl Draw for RmsMeter { fn draw (&self, to: &mut TuiOut) { let XYWH(x, y, w, h) = to.area(); @@ -2415,15 +1938,10 @@ mod audio { #[cfg(feature = "track")] mod track { use crate::*; - impl_as_ref!(Vec: |self: App| self.project.as_ref()); impl_as_mut!(Vec: |self: App| self.project.as_mut()); - impl_as_ref!(Vec: |self: Arrangement| self.tracks); - impl_as_mut!(Vec: |self: Arrangement| self.tracks); - - #[cfg(all(feature = "select"))] impl_as_ref!(Option: |self: App| self.project.as_ref()); - #[cfg(all(feature = "select"))] impl_as_mut!(Option: |self: App| self.project.as_mut()); - + #[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt()); + #[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt()); impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } } @@ -2433,11 +1951,6 @@ mod audio { fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } } - impl>> HasTrack for T { - fn track (&self) -> Option<&Track> { self.get() } - fn track_mut (&mut self) -> Option<&mut Track> { self.get_mut() } - } - impl Track { /// Create a new track with only the default [Sequencer]. pub fn new ( @@ -2542,35 +2055,18 @@ mod audio { #[cfg(feature = "scene")] mod scene { use crate::*; - - impl AddScene for T {} - impl> + AsMut> + Send + Sync> HasScenes for T {} - impl> + AsMut> + Send + Sync> HasScene for T {} - - impl_as_ref!(Vec: |self: App| self.project.as_ref()); - impl_as_mut!(Vec: |self: App| self.project.as_mut()); - impl_as_ref!(Vec: |self: Arrangement| self.scenes.as_ref()); - impl_as_mut!(Vec: |self: Arrangement| self.scenes.as_mut()); - #[cfg(all(feature = "select"))] impl_as_ref!(Option: |self: App| self.project.as_ref()); - #[cfg(all(feature = "select"))] impl_as_mut!(Option: |self: App| self.project.as_mut()); - #[cfg(all(feature = "select"))] impl_as_ref!(Option: |self: Arrangement| self.selected.scene()); - #[cfg(all(feature = "select"))] impl_as_mut!(Option: |self: Arrangement| self.selected.scene()); - + #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt()); + #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); + #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene()); + #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut()); impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } - - impl ScenesView for App { - fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(self.w_side()) } - fn w_side (&self) -> u16 { 20 } + fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(self.w_side()) } + fn w_side (&self) -> u16 { 20 } fn h_scenes (&self) -> u16 { (self.measure_height() as u16).saturating_sub(20) } } - impl Scene { - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } /// Returns the pulse length of the longest clip in the scene pub fn pulses (&self) -> usize { self.clips.iter().fold(0, |a, p|{ @@ -2598,14 +2094,21 @@ mod audio { pub fn clip (&self, index: usize) -> Option<&Arc>> { match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } } + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } } } #[cfg(feature = "sequencer")] mod sequencer { use crate::*; - - impl + AsMut> HasSequencer for T {} - + impl_has!(Sequencer: |self: Track| self.sequencer); + #[cfg(feature = "clock")] impl_has!(Clock: |self: Sequencer|self.clock); + #[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_ins); + #[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_outs); + impl_has!(Measure: |self: MidiEditor| self.size); + impl_has!(Measure: |self: PianoHorizontal| self.size); impl Default for Sequencer { fn default () -> Self { Self { @@ -2626,81 +2129,6 @@ mod audio { } } } - - impl HasMidiBuffers for Sequencer { - fn note_buf_mut (&mut self) -> &mut Vec { &mut self.note_buf } - fn midi_buf_mut (&mut self) -> &mut Vec>> { &mut self.midi_buf } - } - - impl std::fmt::Debug for Sequencer { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Sequencer") - .field("clock", &self.clock) - .field("play_clip", &self.play_clip) - .field("next_clip", &self.next_clip) - .finish() - } - } - - impl MidiMonitor for Sequencer { - fn notes_in (&self) -> &Arc> { - &self.notes_in - } - fn monitoring (&self) -> bool { - self.monitoring - } - fn monitoring_mut (&mut self) -> &mut bool { - &mut self.monitoring - } - } - - impl MidiRecord for Sequencer { - fn recording (&self) -> bool { - self.recording - } - fn recording_mut (&mut self) -> &mut bool { - &mut self.recording - } - fn overdub (&self) -> bool { - self.overdub - } - fn overdub_mut (&mut self) -> &mut bool { - &mut self.overdub - } - } - - #[cfg(feature="clip")] impl HasPlayClip for Sequencer { - fn reset (&self) -> bool { - self.reset - } - fn reset_mut (&mut self) -> &mut bool { - &mut self.reset - } - fn play_clip (&self) -> &Option<(Moment, Option>>)> { - &self.play_clip - } - fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.play_clip - } - fn next_clip (&self) -> &Option<(Moment, Option>>)> { - &self.next_clip - } - fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.next_clip - } - } - - /// JACK process callback for a sequencer's clip sequencer/recorder. - impl Audio for Sequencer { - fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - if self.clock().is_rolling() { - self.process_rolling(scope) - } else { - self.process_stopped(scope) - } - } - } - impl Sequencer { pub fn new ( name: impl AsRef, @@ -2851,6 +2279,56 @@ mod audio { } } } + impl HasMidiBuffers for Sequencer { + fn note_buf_mut (&mut self) -> &mut Vec { &mut self.note_buf } + fn midi_buf_mut (&mut self) -> &mut Vec>> { &mut self.midi_buf } + } + impl std::fmt::Debug for Sequencer { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Sequencer") + .field("clock", &self.clock) + .field("play_clip", &self.play_clip) + .field("next_clip", &self.next_clip) + .finish() + } + } + impl MidiMonitor for Sequencer { + fn monitoring (&self) -> bool { self.monitoring } + fn monitoring_mut (&mut self) -> &mut bool { &mut self.monitoring } + fn notes_in (&self) -> &Arc> { &self.notes_in } + } + impl MidiRecord for Sequencer { + fn recording (&self) -> bool { self.recording } + fn recording_mut (&mut self) -> &mut bool { &mut self.recording } + fn overdub (&self) -> bool { self.overdub } + fn overdub_mut (&mut self) -> &mut bool { &mut self.overdub } + } + #[cfg(feature="clip")] impl HasPlayClip for Sequencer { + fn reset (&self) -> bool { self.reset } + fn reset_mut (&mut self) -> &mut bool { &mut self.reset } + fn play_clip (&self) -> &Option<(Moment, Option>>)> { + &self.play_clip + } + fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.play_clip + } + fn next_clip (&self) -> &Option<(Moment, Option>>)> { + &self.next_clip + } + fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.next_clip + } + } + /// JACK process callback for a sequencer's clip sequencer/recorder. + impl Audio for Sequencer { + fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + if self.clock().is_rolling() { + self.process_rolling(scope) + } else { + self.process_stopped(scope) + } + } + } impl Draw for MidiEditor { fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } } @@ -2859,12 +2337,457 @@ mod audio { } } +#[cfg(feature = "editor")] mod editor { + use crate::*; + impl std::fmt::Debug for MidiEditor { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("MidiEditor").field("mode", &self.mode).finish() + } + } + impl_from!(MidiEditor: |clip: &Arc>| { + let model = Self::from(Some(clip.clone())); + model.redraw(); + model + }); + impl_from!(MidiEditor: |clip: Option>>| { + let mut model = Self::default(); + *model.clip_mut() = clip; + model.redraw(); + model + }); + impl MidiEditor { + /// Put note at current position + pub fn put_note (&mut self, advance: bool) { + let mut redraw = false; + if let Some(clip) = self.clip() { + let mut clip = clip.write().unwrap(); + let note_start = self.get_time_pos(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let note_end = note_start + (note_len.saturating_sub(1)); + let key: u7 = u7::from(note_pos as u8); + let vel: u7 = 100.into(); + let length = clip.length; + let note_end = note_end % length; + let note_on = MidiMessage::NoteOn { key, vel }; + if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { + clip.notes[note_start].push(note_on); + } + let note_off = MidiMessage::NoteOff { key, vel }; + if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { + clip.notes[note_end].push(note_off); + } + if advance { + self.set_time_pos((note_end + 1) % clip.length); + } + redraw = true; + } + if redraw { + self.mode.redraw(); + } + } + fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } + fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } + fn note_length (&self) -> usize { self.get_note_len() } + fn note_pos (&self) -> usize { self.get_note_pos() } + fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } + fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } + fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } + fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } + fn note_len (&self) -> usize { self.get_note_len() } + fn note_len_next (&self) -> usize { self.get_note_len() + 1 } + fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } + fn note_range (&self) -> usize { self.get_note_axis() } + fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } + fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } + fn time_zoom (&self) -> usize { self.get_time_zoom() } + fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_lock (&self) -> bool { self.get_time_lock() } + fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } + fn time_pos (&self) -> usize { self.get_time_pos() } + fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } + fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } + fn time_pos_prev (&self) -> usize { + let step = self.get_note_len(); + self.get_time_pos().overflowing_sub(step) + .0.min(self.clip_length().saturating_sub(step)) + } + fn time_pos_prev_fine (&self) -> usize { + self.get_time_pos().overflowing_sub(1) + .0.min(self.clip_length().saturating_sub(1)) + } + pub fn clip_status (&self) -> impl Content + '_ { + let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.name.clone(), clip.length, clip.looped) + } else { (ItemTheme::G[64], String::new().into(), 0, false) }; + Fixed::X(20, col!( + Fill::X(Align::w(Bsp::e( + button_2("f2", "name ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), + Fill::X(Align::w(Bsp::e( + button_2("l", "ength ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), + Fill::X(Align::w(Bsp::e( + button_2("r", "epeat ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), + )) + } + pub fn edit_status (&self) -> impl Content + '_ { + let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.length) + } else { (ItemTheme::G[64], 0) }; + let time_pos = self.get_time_pos(); + let time_zoom = self.get_time_zoom(); + let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; + let note_pos = self.get_note_pos(); + let note_name = format!("{:4}", note_pitch_to_name(note_pos)); + let note_pos = format!("{:>3}", note_pos); + let note_len = format!("{:>4}", self.get_note_len()); + Fixed::X(20, col!( + Fill::X(Align::w(Bsp::e( + button_2("t", "ime ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + format!("{length} /{time_zoom} +{time_pos} "))))))), + Fill::X(Align::w(Bsp::e( + button_2("z", "lock ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + format!("{time_lock}"))))))), + Fill::X(Align::w(Bsp::e( + button_2("x", "note ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + format!("{note_name} {note_pos} {note_len}"))))))), + )) + } + } + + impl TimeRange for MidiEditor { + fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } + } + + impl NoteRange for MidiEditor { + fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } + } + + impl NotePoint for MidiEditor { + fn note_len (&self) -> &AtomicUsize { self.mode.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } + } + + impl TimePoint for MidiEditor { + fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } + } + + impl MidiViewer for MidiEditor { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } + fn redraw (&self) { self.mode.redraw() } + fn clip (&self) -> &Option>> { self.mode.clip() } + fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } + fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } + } + + impl Layout for MidiEditor { + fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } + } + + impl HasContent for MidiEditor { + fn content (&self) -> impl Content { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } + } + + + impl PianoHorizontal { + pub fn new (clip: Option<&Arc>>) -> Self { + let size = Measure::new(0, 0); + let mut range = MidiSelection::from((12, true)); + range.time_axis = size.x.clone(); + range.note_axis = size.y.clone(); + let piano = Self { + keys_width: 5, + size, + range, + buffer: RwLock::new(Default::default()).into(), + point: MidiCursor::default(), + clip: clip.cloned(), + color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), + }; + piano.redraw(); + piano + } + } + + impl Layout for PianoHorizontal { + fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } + } + impl HasContent for PianoHorizontal { + fn content (&self) -> impl Content { + Bsp::s( + Bsp::e(Fixed::X(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), + Bsp::e(self.keys(), self.size.of(Bsp::b(Fill::XY(self.notes()), Fill::XY(self.cursor())))), + ) + } + } + + impl PianoHorizontal { + /// Draw the piano roll background. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { + for (y, note) in (0..=127).rev().enumerate() { + for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { + let cell = buf.get_mut(x, y).unwrap(); + if note == (127-note_point) || time == time_point { + cell.set_bg(Rgb(0,0,0)); + } else { + cell.set_bg(clip.color.darkest.rgb); + } + if time % 384 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('│'); + } else if time % 96 == 0 { + cell.set_fg(clip.color.dark.rgb); + cell.set_char('╎'); + } else if time % note_len == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('┊'); + } else if (127 - note) % 12 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('='); + } else if (127 - note) % 6 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('—'); + } else { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('·'); + } + } + } + } + /// Draw the piano roll foreground. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { + let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); + let mut notes_on = [false;128]; + for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { + for (_y, note) in (0..=127).rev().enumerate() { + if let Some(cell) = buf.get_mut(x, note) { + if notes_on[note] { + cell.set_char('▂'); + cell.set_style(style); + } + } + } + let time_end = time_start + zoom; + for time in time_start..time_end.min(clip.length) { + for event in clip.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + if let Some(cell) = buf.get_mut(x, note) { + cell.set_char('█'); + cell.set_style(style); + } + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} + } + } + } + + } + } + fn notes (&self) -> impl Content { + let time_start = self.get_time_start(); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + let buffer = self.buffer.clone(); + Thunk::new(move|to: &mut TuiOut|{ + let source = buffer.read().unwrap(); + let XYWH(x0, y0, w, _h) = to.area(); + //if h as usize != note_axis { + //panic!("area height mismatch: {h} <> {note_axis}"); + //} + for (area_x, screen_x) in (x0..x0+w).enumerate() { + for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { + let source_x = time_start + area_x; + let source_y = note_hi - area_y; + // TODO: enable loop rollover: + //let source_x = (time_start + area_x) % source.width.max(1); + //let source_y = (note_hi - area_y) % source.height.max(1); + let is_in_x = source_x < source.width; + let is_in_y = source_y < source.height; + if is_in_x && is_in_y { + if let Some(source_cell) = source.get(source_x, source_y) { + if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { + *cell = source_cell.clone(); + } + } + } + } + } + }) + } + fn cursor (&self) -> impl Content { + let note_hi = self.get_note_hi(); + let note_lo = self.get_note_lo(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let time_pos = self.get_time_pos(); + let time_start = self.get_time_start(); + let time_zoom = self.get_time_zoom(); + let style = Some(Style::default().fg(self.color.lightest.rgb)); + Thunk::new(move|to: &mut TuiOut|{ + let XYWH(x0, y0, w, _) = to.area(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + if note == note_pos { + for x in 0..w { + let screen_x = x0 + x; + let time_1 = time_start + x as usize * time_zoom; + let time_2 = time_1 + time_zoom; + if time_1 <= time_pos && time_pos < time_2 { + to.blit(&"█", screen_x, screen_y, style); + let tail = note_len as u16 / time_zoom as u16; + for x_tail in (screen_x + 1)..(screen_x + tail) { + to.blit(&"▂", x_tail, screen_y, style); + } + break + } + } + break + } + } + }) + } + fn keys (&self) -> impl Content { + let state = self; + let color = state.color; + let note_lo = state.get_note_lo(); + let note_hi = state.get_note_hi(); + let note_pos = state.get_note_pos(); + let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); + let off_style = Some(Style::default().fg(Tui::g(255))); + let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); + Fill::Y(Fixed::X(self.keys_width, Thunk::new(move|to: &mut TuiOut|{ + let XYWH(x, y0, _w, _h) = to.area(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + to.blit(&to_key(note), x, screen_y, key_style); + if note > 127 { + continue + } + if note == note_pos { + to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style) + } else { + to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) + }; + } + }))) + } + fn timeline (&self) -> impl Content + '_ { + Fill::X(Fixed::Y(1, Thunk::new(move|to: &mut TuiOut|{ + let XYWH(x, y, w, _h) = to.area(); + let style = Some(Style::default().dim()); + let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { + let t = area_x as usize * self.time_zoom().get(); + if t < length { + to.blit(&"|", screen_x, y, style); + } + } + }))) + } + } + + impl TimeRange for PianoHorizontal { + fn time_len (&self) -> &AtomicUsize { self.range.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.range.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } + } + + impl NoteRange for PianoHorizontal { + fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } + } + + impl NotePoint for PianoHorizontal { + fn note_len (&self) -> &AtomicUsize { self.point.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } + } + + impl TimePoint for PianoHorizontal { + fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } + } + + impl MidiViewer for PianoHorizontal { + fn clip (&self) -> &Option>> { &self.clip } + fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } + /// Determine the required space to render the clip. + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { (clip.length / self.range.time_zoom().get(), 128) } + fn redraw (&self) { + *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { + let clip = clip.read().unwrap(); + let buf_size = self.buffer_size(&clip); + let mut buffer = BigBuffer::from(buf_size); + let time_zoom = self.get_time_zoom(); + self.time_len().set(clip.length); + PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); + PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); + buffer + } else { + Default::default() + } + } + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); + self.redraw(); + } + } + + impl std::fmt::Debug for PianoHorizontal { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + let buffer = self.buffer.read().unwrap(); + f.debug_struct("PianoHorizontal") + .field("time_zoom", &self.range.time_zoom) + .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) + .finish() + } + } + impl OctaveVertical { + fn color (&self, pitch: usize) -> Color { + let pitch = pitch % 12; + self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] + } + } + impl HasContent for OctaveVertical { + fn content (&self) -> impl Content { + row!( + Tui::fg_bg(self.color(0), self.color(1), "▙"), + Tui::fg_bg(self.color(2), self.color(3), "▙"), + Tui::fg_bg(self.color(4), self.color(5), "▌"), + Tui::fg_bg(self.color(6), self.color(7), "▟"), + Tui::fg_bg(self.color(8), self.color(9), "▟"), + Tui::fg_bg(self.color(10), self.color(11), "▟"), + ) + } + } +} + #[cfg(feature = "sampler")] mod sampler { use crate::*; impl Default for SampleKit { - fn default () -> Self { - Self([const { None }; N]) - } + fn default () -> Self { Self([const { None }; N]) } } impl Iterator for Voice { type Item = [f32;2]; @@ -3411,11 +3334,8 @@ mod audio { } #[cfg(feature = "lv2")] mod lv2 { - use crate::*; - impl_audio!(Lv2: lv2_jack_process); - impl Lv2 { const INPUT_BUFFER: usize = 1024; pub fn new ( @@ -3454,7 +3374,6 @@ mod audio { }) } } - fn lv2_jack_process ( Lv2 { midi_ins, midi_outs, audio_ins, audio_outs, @@ -3569,7 +3488,7 @@ mod pool { use crate::*; has_clips!(|self: Pool|self.clips); has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); - from!(Pool: |clip:&Arc>|{ + impl_from!(Pool: |clip:&Arc>|{ let model = Self::default(); model.clips.write().unwrap().push(clip.clone()); model.clip.store(1, Relaxed); diff --git a/app/tek_struct.rs b/app/tek_struct.rs index bf1fa768..34d2277f 100644 --- a/app/tek_struct.rs +++ b/app/tek_struct.rs @@ -5,7 +5,7 @@ use builder_pattern::Builder; /// Total state /// /// ``` -/// use tek::{TracksView, ScenesView, AddScene}; +/// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; /// let mut app = tek::App::default(); /// let _ = app.scene_add(None, None).unwrap(); /// let _ = app.update_clock(); diff --git a/app/tek_trait.rs b/app/tek_trait.rs index 4e738b82..2ee87476 100644 --- a/app/tek_trait.rs +++ b/app/tek_trait.rs @@ -125,24 +125,55 @@ pub trait HasMidiClip { fn clip (&self) -> Option>>; } pub trait HasClock: AsRef + AsMut { - fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } fn clock (&self) -> &Clock { self.as_ref() } + fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } } pub trait HasDevices: AsRef> + AsMut> { - fn devices_mut (&mut self) -> &mut Vec { self.as_mut() } fn devices (&self) -> &Vec { self.as_ref() } -} -pub trait HasSelection: AsRef + AsMut { - fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } - fn selection (&self) -> &Selection { 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 HasScene: AsRef> + AsMut> { - fn scene_mut (&mut self) -> &mut Option { self.as_mut() } - fn scene (&self) -> Option<&Scene> { 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() } @@ -150,12 +181,30 @@ pub trait HasScenes: AsRef> + AsMut> { /// 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) } -} -pub trait HasSceneScroll: HasScenes { - fn scene_scroll (&self) -> usize; -} -pub trait HasTrackScroll: HasTracks { - fn track_scroll (&self) -> usize; + /// 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; @@ -170,39 +219,26 @@ pub trait HasMidiBuffers { } /// ``` -/// use tek::{MidiEditor, HasEditor, tengri::Has}; +/// use tek::{*, tengri::*}; /// -/// let mut host = TestEditorHost(Some(MidiEditor::default())); -/// struct TestEditorHost(Option); -/// impl AsRef> for TestEditorHost { -/// fn get (&self) -> &Option { &self.0 } -/// fn get_mut (&mut self) -> &mut Option { &mut self.0 } -/// } +/// 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: AsRef> { - fn editor (&self) -> Option<&MidiEditor> { - self.get().as_ref() - } - fn editor_mut (&mut self) -> Option<&mut MidiEditor> { - self.get_mut().as_mut() - } - 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 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>; @@ -212,7 +248,6 @@ pub trait HasClips { (self.clips().len() - 1, clip) } } - /// Trait for thing that may receive MIDI. pub trait HasMidiIns { fn midi_ins (&self) -> &Vec; @@ -237,7 +272,6 @@ pub trait HasMidiIns { }) } } - /// Trait for thing that may output MIDI. pub trait HasMidiOuts { fn midi_outs (&self) -> &Vec; @@ -259,9 +293,7 @@ pub trait HasMidiOuts { } } } - -impl> + AsMut> + Send + Sync> HasTracks for T {} -pub trait HasTracks: AsRef> + AsMut> + Send + Sync { +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 @@ -292,10 +324,9 @@ pub trait HasTracks: AsRef> + AsMut> + Send + Sync { /// Spacing between tracks. const TRACK_SPACING: usize = 0; } - -pub trait HasTrack { - fn track (&self) -> Option<&Track>; - fn track_mut (&mut self) -> Option<&mut Track>; +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 Content + 'a { self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) } @@ -523,33 +554,6 @@ pub trait MidiViewer: Measured + MidiRange + MidiPoint + Debug + Send + } } -pub trait AddScene: HasScenes + HasTracks { - /// Add multiple scenes - fn scenes_add (&mut self, n: usize) -> Usually<()> { - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..n { - let _ = self.scene_add(None, Some( - scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() - ))?; - } - Ok(()) - } - /// Add a scene - fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<(usize, &mut Scene)> - { - let scene = Scene { - name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), - clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(ItemTheme::random), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok((index, &mut self.scenes_mut()[index])) - } -} - pub trait ClipsView: TracksView + ScenesView { fn view_scenes_clips <'a> (&'a self) diff --git a/tengri b/tengri index c1b727ba..d1c08df5 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit c1b727bafc27dc715d7239a0bc63a1cdfe962bc2 +Subproject commit d1c08df5351ce8c3913723602a05268d593c9a45