From 89288f2920172ca28354025b7812bcc9da7b4001 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 14 May 2025 00:46:33 +0300 Subject: [PATCH 01/12] wip: refactor arranger to device --- config/config_arranger.edn | 2 +- config/config_groovebox.edn | 2 +- config/config_sequencer.edn | 2 +- config/keys_arranger.edn | 2 +- config/keys_device_add.edn | 5 +- crates/app/src/api.rs | 308 --------- crates/app/src/audio.rs | 83 +-- crates/app/src/model.rs | 615 ++++++------------ crates/app/src/model/dialog.rs | 36 - crates/app/src/model/pool.rs | 0 crates/app/src/model/scene.rs | 76 --- crates/app/src/model/track.rs | 161 ----- crates/app/src/view.rs | 559 ++-------------- crates/app/src/view/view_dialog.rs | 99 --- crates/app/src/view/view_output.rs | 92 --- crates/cli/tek.rs | 43 +- crates/device/Cargo.toml | 3 +- crates/device/src/arranger.rs | 86 +++ crates/device/src/arranger/arranger_api.rs | 207 ++++++ crates/device/src/arranger/arranger_audio.rs | 7 + crates/device/src/arranger/arranger_clip.rs | 35 + crates/device/src/arranger/arranger_device.rs | 27 + crates/device/src/arranger/arranger_edit.rs | 62 ++ crates/device/src/arranger/arranger_model.rs | 74 +++ crates/device/src/arranger/arranger_port.rs | 134 ++++ crates/device/src/arranger/arranger_scene.rs | 261 ++++++++ .../src/arranger/arranger_select.rs} | 74 ++- crates/device/src/arranger/arranger_track.rs | 290 +++++++++ crates/device/src/arranger/arranger_view.rs | 217 ++++++ crates/device/src/browser/browser_api.rs | 30 +- crates/device/src/browser/browser_model.rs | 16 +- crates/device/src/lib.rs | 7 +- crates/device/src/sequencer/seq_audio.rs | 4 +- crates/device/src/sequencer/seq_model.rs | 13 +- crates/engine/src/jack/jack_port.rs | 176 ++--- crates/engine/src/lib.rs | 21 +- crates/engine/src/midi.rs | 28 +- crates/engine/src/midi/midi_in.rs | 42 ++ crates/engine/src/midi/midi_out.rs | 33 + deps/tengri | 2 +- 40 files changed, 2015 insertions(+), 1919 deletions(-) delete mode 100644 crates/app/src/model/dialog.rs delete mode 100644 crates/app/src/model/pool.rs delete mode 100644 crates/app/src/model/scene.rs delete mode 100644 crates/app/src/model/track.rs delete mode 100644 crates/app/src/view/view_dialog.rs delete mode 100644 crates/app/src/view/view_output.rs create mode 100644 crates/device/src/arranger.rs create mode 100644 crates/device/src/arranger/arranger_api.rs create mode 100644 crates/device/src/arranger/arranger_audio.rs create mode 100644 crates/device/src/arranger/arranger_clip.rs create mode 100644 crates/device/src/arranger/arranger_device.rs create mode 100644 crates/device/src/arranger/arranger_edit.rs create mode 100644 crates/device/src/arranger/arranger_model.rs create mode 100644 crates/device/src/arranger/arranger_port.rs create mode 100644 crates/device/src/arranger/arranger_scene.rs rename crates/{app/src/model/selection.rs => device/src/arranger/arranger_select.rs} (67%) create mode 100644 crates/device/src/arranger/arranger_track.rs create mode 100644 crates/device/src/arranger/arranger_view.rs create mode 100644 crates/engine/src/midi/midi_in.rs create mode 100644 crates/engine/src/midi/midi_out.rs diff --git a/config/config_arranger.edn b/config/config_arranger.edn index a35182bf..cffba38f 100644 --- a/config/config_arranger.edn +++ b/config/config_arranger.edn @@ -13,7 +13,7 @@ (keys (layer-if :focus-message "./keys_message.edn") (layer-if :focus-device-add "./keys_device_add.edn") - (layer-if :focus-browser "./keys_browser") + (layer-if :focus-browser "./keys_browser.edn") (layer-if :focus-pool-rename "./keys_rename.edn") (layer-if :focus-pool-length "./keys_length.edn") (layer "./keys_global.edn") diff --git a/config/config_groovebox.edn b/config/config_groovebox.edn index 67b0e5f9..fec1ca2d 100644 --- a/config/config_groovebox.edn +++ b/config/config_groovebox.edn @@ -15,7 +15,7 @@ (fill/y :view-editor))))))))))) (keys - (layer-if :focus-browser "./keys_browser") + (layer-if :focus-browser "./keys_browser.edn") (layer-if :focus-pool-rename "./keys_rename.edn") (layer-if :focus-pool-length "./keys_length.edn") (layer "./keys_global.edn") diff --git a/config/config_sequencer.edn b/config/config_sequencer.edn index e09c0e7b..f20e2fcd 100644 --- a/config/config_sequencer.edn +++ b/config/config_sequencer.edn @@ -11,7 +11,7 @@ :view-editor))))) (keys - (layer-if :focus-browser "./keys_browser") + (layer-if :focus-browser "./keys_browser.edn") (layer-if :mode-pool-rename "./keys_rename.edn") (layer-if :mode-pool-length "./keys_length.edn") (layer "./keys_global.edn") diff --git a/config/keys_arranger.edn b/config/keys_arranger.edn index 4f52a767..40ffc742 100644 --- a/config/keys_arranger.edn +++ b/config/keys_arranger.edn @@ -6,7 +6,7 @@ (@shift-O output add) (@shift-S scene add) (@shift-T track add) -(@shift-D device picker) +(@shift-D toggle-dialog :dialog-device) (@up select :select-scene-prev) (@down select :select-scene-next) diff --git a/config/keys_device_add.edn b/config/keys_device_add.edn index 6e557b0b..9ad4b197 100644 --- a/config/keys_device_add.edn +++ b/config/keys_device_add.edn @@ -1,3 +1,2 @@ -(@up device pick :device-kind-prev) -(@down device pick :device-kind-next) -(@enter device add :device-kind) +(@up toggle-dialog :dialog-device-prev) +(@down toggle-dialog :dialog-device-next) diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index 28076419..17ef811a 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -17,173 +17,6 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm None })); -#[tengri_proc::expose] impl App { - fn _todo_isize_stub (&self) -> isize { - todo!() - } - fn _todo_item_theme_stub (&self) -> ItemTheme { - todo!() - } - fn focus_editor (&self) -> bool { - self.is_editing() - } - fn focus_message (&self) -> bool { - matches!(self.dialog, Some(Dialog::Message(..))) - } - fn focus_device_add (&self) -> bool { - matches!(self.dialog, Some(Dialog::Device(..))) - } - fn focus_clip (&self) -> bool { - !self.is_editing() && self.selected.is_clip() - } - fn focus_track (&self) -> bool { - !self.is_editing() && self.selected.is_track() - } - fn focus_scene (&self) -> bool { - !self.is_editing() && self.selected.is_scene() - } - fn focus_mix (&self) -> bool { - !self.is_editing() && self.selected.is_mix() - } - fn focus_pool_import (&self) -> bool { - matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Import(..))) - } - fn focus_pool_export (&self) -> bool { - matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Export(..))) - } - fn focus_pool_rename (&self) -> bool { - matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Rename(..))) - } - fn focus_pool_length (&self) -> bool { - matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Length(..))) - } - fn dialog_help (&self) -> Dialog { - Dialog::Help - } - fn dialog_menu (&self) -> Dialog { - Dialog::Menu - } - fn dialog_save (&self) -> Dialog { - Dialog::Save - } - fn dialog_load (&self) -> Dialog { - Dialog::Load - } - fn dialog_options (&self) -> Dialog { - Dialog::Options - } - fn editor_pitch (&self) -> Option { - Some((self.editor().map(|e|e.get_note_pos()).unwrap() as u8).into()) - } - /// Width of display - pub(crate) fn w (&self) -> u16 { - self.size.w() as u16 - } - /// Width allocated for sidebar. - pub(crate) fn w_sidebar (&self) -> u16 { - self.w() / if self.is_editing() { 16 } else { 8 } as u16 - } - /// Width taken by all tracks. - pub(crate) fn w_tracks (&self) -> u16 { - self.tracks_with_sizes().last().map(|(_, _, _, x)|x as u16).unwrap_or(0) - } - /// Width available to display tracks. - pub(crate) fn w_tracks_area (&self) -> u16 { - self.w().saturating_sub(2 * self.w_sidebar()) - } - /// Height of display - pub(crate) fn h (&self) -> u16 { - self.size.h() as u16 - } - /// Height available to display track headers. - pub(crate) fn h_tracks_area (&self) -> u16 { - 5 // FIXME - //self.h().saturating_sub(self.h_inputs() + self.h_outputs()) - } - /// Height available to display tracks. - pub(crate) fn h_scenes_area (&self) -> u16 { - //15 - self.h().saturating_sub( - self.h_inputs() + - self.h_outputs() + - self.h_devices() + - 13 // FIXME - ) - } - /// Height taken by all scenes. - pub(crate) fn h_scenes (&self) -> u16 { - self.scenes_with_sizes(self.is_editing(), Self::H_SCENE, Self::H_EDITOR).last() - .map(|(_, _, _, y)|y as u16).unwrap_or(0) - } - /// Height taken by all inputs. - pub(crate) fn h_inputs (&self) -> u16 { - self.inputs_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) - } - /// Height taken by all outputs. - pub(crate) fn h_outputs (&self) -> u16 { - self.outputs_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) - } - /// Height taken by visible device slots. - pub(crate) fn h_devices (&self) -> u16 { - 2 - //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) - } - fn scene_count (&self) -> usize { - self.scenes.len() - } - fn scene_selected (&self) -> Option { - self.selected.scene() - } - fn track_count (&self) -> usize { - self.tracks.len() - } - fn track_selected (&self) -> Option { - self.selected.track() - } - fn select_scene_next (&self) -> Selection { - self.selected.scene_next(self.scenes.len()) - } - fn select_scene_prev (&self) -> Selection { - self.selected.scene_prev() - } - fn select_track_header (&self) -> Selection { - self.selected.track_header(self.tracks.len()) - } - fn select_track_next (&self) -> Selection { - self.selected.track_next(self.tracks.len()) - } - fn select_track_prev (&self) -> Selection { - self.selected.track_prev() - } - fn clip_selected (&self) -> Option>> { - match self.selected { - Selection::TrackClip { track, scene } => self.scenes[scene].clips[track].clone(), - _ => None - } - } - fn device_kind (&self) -> usize { - if let Some(Dialog::Device(index)) = self.dialog { - index - } else { - 0 - } - } - fn device_kind_prev (&self) -> usize { - if let Some(Dialog::Device(index)) = self.dialog { - index.overflowing_sub(1).0.min(self.device_kinds().len().saturating_sub(1)) - } else { - 0 - } - } - fn device_kind_next (&self) -> usize { - if let Some(Dialog::Device(index)) = self.dialog { - (index + 1) % self.device_kinds().len() - } else { - 0 - } - } -} - #[tengri_proc::command(App)] impl AppCommand { fn toggle_dialog (app: &mut App, dialog: Dialog) -> Perhaps { app.toggle_dialog(Some(dialog)); @@ -313,150 +146,9 @@ impl<'state> Context<'state, SamplerCommand> for App { } } -#[tengri_proc::command(App)] impl InputCommand { - fn add (app: &mut App) -> Perhaps { - app.midi_in_add()?; - Ok(None) - } -} - -#[tengri_proc::command(App)] impl OutputCommand { - fn add (app: &mut App) -> Perhaps { - app.midi_out_add()?; - Ok(None) - } -} - -#[tengri_proc::command(App)] impl DeviceCommand { - fn cancel (app: &mut App) -> Perhaps { - app.dialog = None; - Ok(None) - } - fn picker (app: &mut App) -> Perhaps { - app.device_picker_show(); - Ok(None) - } - fn pick (app: &mut App, i: usize) -> Perhaps { - app.device_pick(i); - Ok(None) - } - fn add (app: &mut App, i: usize) -> Perhaps { - app.device_add(i); - Ok(None) - } -} - #[tengri_proc::command(App)] impl MessageCommand { fn dismiss (app: &mut App) -> Perhaps { app.message_dismiss(); Ok(None) } } - -#[tengri_proc::command(App)] impl TrackCommand { - fn toggle_play (app: &mut App) -> Perhaps { - todo!() - } - fn toggle_solo (app: &mut App) -> Perhaps { - todo!() - } - fn toggle_rec (app: &mut App) -> Perhaps { - app.track_toggle_record(); - Ok(Some(Self::ToggleRec)) - } - fn toggle_mon (app: &mut App) -> Perhaps { - app.track_toggle_monitor(); - Ok(Some(Self::ToggleMon)) - } - fn set_size (app: &mut App, size: usize) -> Perhaps { - todo!() - } - fn set_zoom (app: &mut App, zoom: usize) -> Perhaps { - todo!() - } - fn swap (app: &mut App, index: usize, other: usize) -> Perhaps { - todo!(); - Ok(Some(Self::Swap { index, other })) - } - fn del (app: &mut App, index: usize) -> Perhaps { - app.track_del(index); - Ok(None) - } - fn stop (app: &mut App, index: usize) -> Perhaps { - app.tracks[index].sequencer.enqueue_next(None); - Ok(None) - } - fn add (app: &mut App) -> Perhaps { - Ok(Some(Self::Del { index: app.track_add_focus()? })) - } - fn set_color (app: &mut App, index: usize, color: ItemTheme) -> Perhaps { - Ok(Some(Self::SetColor { index, color: app.track_set_color(index, color) })) - } -} - -#[tengri_proc::command(App)] impl SceneCommand { - fn add (app: &mut App) -> Perhaps { - todo!() - } - fn del (app: &mut App, index: usize) -> Perhaps { - app.scene_del(index); - Ok(None) - } - fn enqueue (app: &mut App, index: usize) -> Perhaps { - app.scene_enqueue(index); - Ok(None) - } - fn set_color (app: &mut App, index: usize, color: ItemTheme) -> Perhaps { - Ok(Some(Self::SetColor { index, color: app.scene_set_color(index, color) })) - } - fn set_size (app: &mut App, index: usize, size: usize) -> Perhaps { - todo!() - } - fn set_zoom (app: &mut App, index: usize, zoom: usize) -> Perhaps { - todo!() - } - fn swap (app: &mut App, index: usize, other: usize) -> Perhaps { - todo!(); - Ok(Some(Self::Swap { index, other })) - } -} - -#[tengri_proc::command(App)] impl ClipCommand { - fn get (app: &mut App, a: usize, b: usize) -> Perhaps { - //(Get [a: usize, b: usize] cmd_todo!("\n\rtodo: clip: get: {a} {b}")) - //("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap()))) - todo!() - } - fn edit (app: &mut App, a: usize, b: usize) -> Perhaps { - //(Edit [clip: MaybeClip] cmd_todo!("\n\rtodo: clip: edit: {clip:?}")) - //("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap()))) - todo!() - } - fn set_loop (app: &mut App, a: usize, b: usize) -> Perhaps { - //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) - //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) - todo!() - } - fn put (app: &mut App, a: usize, b: usize) -> Perhaps { - //(Put [t: usize, s: usize, c: MaybeClip] - //Some(Self::Put(t, s, app.clip_put(t, s, c)))) - //("put" [a: usize, b: usize, c: MaybeClip] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap()))) - todo!() - } - fn del (app: &mut App, a: usize, b: usize) -> Perhaps { - //("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None)))) - todo!() - } - fn enqueue (app: &mut App, a: usize, b: usize) -> Perhaps { - //(Enqueue [t: usize, s: usize] - //cmd!(app.tracks[t].sequencer.enqueue_next(app.scenes[s].clips[t].as_ref()))) - //("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap()))) - todo!() - } - fn set_color (app: &mut App, a: usize, b: usize) -> Perhaps { - //(SetColor [t: usize, s: usize, c: ItemTheme] - //app.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); - //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) - todo!() - } -} diff --git a/crates/app/src/audio.rs b/crates/app/src/audio.rs index 39d23f13..15d15232 100644 --- a/crates/app/src/audio.rs +++ b/crates/app/src/audio.rs @@ -1,12 +1,32 @@ use crate::*; +impl HasJack for App { + fn jack (&self) -> &Jack { + &self.jack + } +} + audio!( |self: App, client, scope|{ let t0 = self.perf.get_t0(); self.clock().update_from_scope(scope).unwrap(); - let midi_in = self.collect_midi_input(scope); - self.update_editor_cursor(&midi_in); - let result = self.render_tracks(client, scope); + let midi_in = self.arranger.midi_input_collect(scope); + if let Some(editor) = &self.editor { + let mut pitch: Option = None; + for port in midi_in.iter() { + for event in port.iter() { + if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {ref key, ..}, ..})) + = event + { + pitch = Some(key.clone()); + } + } + } + if let Some(pitch) = pitch { + editor.set_note_pos(pitch.as_int() as usize); + } + } + let result = self.arranger.tracks_render(client, scope); self.perf.update_from_jack_scope(t0, scope); result }; @@ -33,60 +53,3 @@ audio!( } } ); - -type CollectedMidiInput<'a> = Vec, MidiError>)>>; - -impl App { - - /// Collect MIDI input from app ports (TODO preallocate large buffers) - fn collect_midi_input <'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::>() - } - - /// Update cursor in MIDI editor - fn update_editor_cursor (&self, midi_in: &CollectedMidiInput) { - if let Some(editor) = &self.editor { - let mut pitch: Option = None; - for port in midi_in.iter() { - for event in port.iter() { - if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {ref key, ..}, ..})) - = event - { - pitch = Some(key.clone()); - } - } - } - if let Some(pitch) = pitch { - editor.set_note_pos(pitch.as_int() as usize); - } - } - } - - /// Run audio callbacks for every track and every device - fn render_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { - for track in self.tracks.iter_mut() { - if Control::Quit == PlayerAudio( - track.sequencer_mut(), &mut self.note_buf, &mut self.midi_buf - ).process(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 - } - -} - -impl HasJack for App { - fn jack (&self) -> &Jack { - &self.jack - } -} diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index 074ca999..e7411bea 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -1,445 +1,95 @@ use crate::*; -mod dialog; pub use self::dialog::*; -mod pool; pub use self::pool::*; -mod selection; pub use self::selection::*; -mod track; pub use self::track::*; -mod scene; pub use self::scene::*; - #[derive(Default, Debug)] pub struct App { /// Must not be dropped for the duration of the process - pub jack: Jack, + pub jack: Jack, + /// Port handles + pub ports: std::collections::BTreeMap>, /// Source of time pub clock: Clock, - /// Theme - pub color: ItemTheme, - /// Contains all clips in the project - pub pool: Option, - /// Contains the currently edited MIDI clip - pub editor: Option, - /// Contains a render of the project arrangement, redrawn on update. - pub arranger: Arc>, - /// List of global midi inputs - pub midi_ins: Vec, - /// List of global midi outputs - pub midi_outs: Vec, - /// List of global audio inputs - pub audio_ins: Vec, - /// List of global audio outputs - pub audio_outs: Vec, - /// Buffer for writing a midi event - pub note_buf: Vec, - /// Buffer for writing a chunk of midi events - pub midi_buf: Vec>>, - /// Last track number (to avoid duplicate port names) - pub track_last: usize, - /// List of tracks - pub tracks: Vec, - /// Scroll offset of tracks - pub track_scroll: usize, - /// List of scenes - pub scenes: Vec, - /// Scroll offset of scenes - pub scene_scroll: usize, - /// Selected UI element - pub selected: Selection, /// Display size pub size: Measure, /// Performance counter pub perf: PerfModel, - /// Whether in edit mode - pub editing: AtomicBool, + + // View and input definition + pub config: Configuration, /// Undo history - pub history: Vec, - /// Port handles - pub ports: std::collections::BTreeMap>, + pub history: Vec, + // Dialog overlay + pub dialog: Option, + /// Browses external resources, such as directories + pub browser: Option, + /// Contains the currently edited musical arrangement + pub arranger: Option, + /// Contains all clips in the project + pub pool: Option, + /// Contains the currently edited MIDI clip + pub editor: Option, + // Cache of formatted strings pub view_cache: Arc>, - // Dialog overlay - pub dialog: Option, - // View and input definition - pub config: Configuration + /// Base color. + pub color: ItemTheme, } impl App { - - /// Add multiple tracks - pub fn tracks_add ( - &mut self, - count: usize, - width: Option, - mins: &[PortConnect], - mouts: &[PortConnect], - ) -> Usually<()> { - let jack = self.jack().clone(); - let track_color_1 = ItemColor::random(); - let track_color_2 = ItemColor::random(); - for i in 0..count { - let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); - let mut track = self.track_add(None, Some(color), mins, mouts)?.1; - if let Some(width) = width { - track.width = width; - } - } - Ok(()) - } - - /// Add a track - pub fn track_add ( - &mut self, - name: Option<&str>, - color: Option, - mins: &[PortConnect], - mouts: &[PortConnect], - ) -> Usually<(usize, &mut Track)> { - self.track_last += 1; - let name: Arc = name.map_or_else( - ||format!("Track{:02}", self.track_last).into(), - |x|x.to_string().into() - ); - let mut track = Track { - width: (name.len() + 2).max(12), - color: color.unwrap_or_else(ItemTheme::random), - sequencer: Sequencer::new( - &format!("{name}"), - self.jack(), - Some(self.clock()), - None, - mins, - mouts - )?, - name, - ..Default::default() - }; - self.tracks_mut().push(track); - let len = self.tracks().len(); - let index = len - 1; - for scene in self.scenes_mut().iter_mut() { - while scene.clips.len() < len { - scene.clips.push(None); - } - } - Ok((index, &mut self.tracks_mut()[index])) - } - - /// Add and focus a track - pub(crate) fn track_add_focus (&mut self) -> Usually { - use Selection::*; - let index = self.track_add(None, None, &[], &[])?.0; - self.selected = match self.selected { - Track(_) => Track(index), - TrackClip { track, scene } => TrackClip { track: index, scene }, - _ => self.selected - }; - Ok(index) - } - - /// Delete a track - pub fn track_del (&mut self, index: usize) -> Usually<()> { - let exists = self.tracks().get(index).is_some(); - if exists { - let track = self.tracks_mut().remove(index); - let Track { sequencer: Sequencer { midi_ins, midi_outs, .. }, .. } = track; - for port in midi_ins.into_iter() { - port.close()?; - } - for port in midi_outs.into_iter() { - port.close()?; - } - for scene in self.scenes_mut().iter_mut() { - scene.clips.remove(index); - } - } - Ok(()) - } - - /// Add multiple scenes - pub fn scenes_add (&mut self, n: usize) -> Usually<()> { - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..n { - let _ = self.scene_add(None, Some( - scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() - ))?; - } - Ok(()) - } - - /// Add a scene - pub fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<(usize, &mut Scene)> - { - let scene = Scene { - name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), - clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(ItemTheme::random), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok((index, &mut self.scenes_mut()[index])) - } - - /// Add and focus an empty scene - pub fn scene_add_focus (&mut self) -> Usually { - use Selection::*; - let index = self.scene_add(None, None)?.0; - self.selected = match self.selected { - Scene(_) => Scene(index), - TrackClip { track, scene } => TrackClip { track, scene: index }, - _ => self.selected - }; - Ok(index) - } - - /// Enqueue clips from a scene across all tracks - pub fn scene_enqueue (&mut self, scene: usize) { - for track in 0..self.tracks.len() { - self.tracks[track].sequencer.enqueue_next(self.scenes[scene].clips[track].as_ref()); - } - } - - pub fn toggle_editor (&mut self, value: Option) { - let editing = self.is_editing(); - let value = value.unwrap_or_else(||!self.is_editing()); - self.editing.store(value, Relaxed); - if value { - self.clip_auto_create(); + pub fn toggle_dialog (&mut self, dialog: Option) { + self.dialog = if self.dialog == dialog { + None } else { - self.clip_auto_remove(); + dialog } } - - // Create new clip in pool when entering empty cell - pub fn clip_auto_create (&mut self) { - if let Some(ref pool) = self.pool - && let Selection::TrackClip { track, scene } = self.selected - && let Some(scene) = self.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && slot.is_none() - { - let (index, mut clip) = pool.add_new_clip(); - // autocolor: new clip colors from scene and track color - clip.write().unwrap().color = ItemColor::random_near( - self.tracks[track].color.base.mix( - scene.color.base, - 0.5 - ), - 0.2 - ).into(); - if let Some(ref mut editor) = self.editor { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip); - } + pub fn toggle_editor (&mut self, value: Option) { + self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); + self.arranger.map(|arranger|if value { + arranger.clip_auto_create(); + } else { + arranger.clip_auto_remove(); + }); } - - // Remove clip from arrangement when exiting empty clip editor - pub fn clip_auto_remove (&mut self) { - if let Some(ref mut pool) = self.pool - && let Selection::TrackClip { track, scene } = self.selected - && let Some(scene) = self.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && let Some(clip) = slot.as_mut() - { - let mut swapped = None; - if clip.read().unwrap().count_midi_messages() == 0 { - std::mem::swap(&mut swapped, slot); - } - if let Some(clip) = swapped { - pool.delete_clip(&clip.read().unwrap()); - } - } + pub(crate) fn device_pick (&mut self, index: usize) { + self.dialog = Some(Dialog::Device(index)); } - - /// Put a clip in a slot - pub(crate) fn clip_put ( - &mut self, track: usize, scene: usize, clip: Option>> - ) -> Option>> { - let old = self.scenes[scene].clips[track].clone(); - self.scenes[scene].clips[track] = clip; - old - } - - /// Change the color of a clip, returning the previous one - pub(crate) fn clip_set_color ( - &self, track: usize, scene: usize, color: ItemTheme - ) -> Option { - self.scenes[scene].clips[track].as_ref().map(|clip|{ - let mut clip = clip.write().unwrap(); - let old = clip.color.clone(); - clip.color = color.clone(); - panic!("{color:?} {old:?}"); - old - }) - } - - /// Get the clip pool, if present - pub(crate) fn pool (&self) -> Option<&Pool> { - self.pool.as_ref() - } - - /// Get the active clip - pub(crate) fn clip (&self) -> Option>> { - self.scene()?.clips.get(self.selected().track()?)?.clone() - } - - /// Get the active editor - pub(crate) fn editor (&self) -> Option<&MidiEditor> { - self.editor.as_ref() - } - - /// Toggle looping for the active clip - pub(crate) fn toggle_loop (&mut self) { - if let Some(clip) = self.clip() { - clip.write().unwrap().toggle_loop() - } - } - - /// Set the selection - pub(crate) fn select (&mut self, s: Selection) { - self.selected = s; - // autoedit: load focused clip in editor. - if let Some(ref mut editor) = self.editor { - editor.set_clip(match self.selected { - Selection::TrackClip { track, scene } - if let Some(Some(Some(clip))) = self - .scenes.get(scene) - .map(|s|s.clips.get(track)) => Some(clip), - _ => None - }); - } - } - - /// Stop all playing clips - pub(crate) fn stop_all (&mut self) { - for track in 0..self.tracks.len() { - self.tracks[track].sequencer.enqueue_next(None); - } - } - - /// Launch a clip or scene - pub(crate) fn launch (&mut self) { - use Selection::*; - match self.selected { - Track(t) => { - self.tracks[t].sequencer.enqueue_next(None) - }, - TrackClip { track, scene } => { - self.tracks[track].sequencer.enqueue_next(self.scenes[scene].clips[track].as_ref()) - }, - Scene(s) => { - for t in 0..self.tracks.len() { - self.tracks[t].sequencer.enqueue_next(self.scenes[s].clips[t].as_ref()) - } - }, - _ => {} - }; - } - - /// Get the first sampler of the active track - pub fn sampler (&self) -> Option<&Sampler> { - self.track().map(|t|t.sampler(0)).flatten() - } - - /// Get the first sampler of the active track - pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.track_mut().map(|t|t.sampler_mut(0)).flatten() - } - - /// Set the color of the selected entity - pub fn set_color (&mut self, palette: Option) -> Option { - use Selection::*; - let palette = palette.unwrap_or_else(||ItemTheme::random()); - Some(match self.selected { - Mix => { - let old = self.color; - self.color = palette; - old - }, - Scene(s) => { - let old = self.scenes[s].color; - self.scenes[s].color = palette; - old - } - Track(t) => { - let old = self.tracks[t].color; - self.tracks[t].color = palette; - old - } - TrackClip { track, scene } => { - if let Some(ref clip) = self.scenes[scene].clips[track] { - let mut clip = clip.write().unwrap(); - let old = clip.color; - clip.color = palette; - old - } else { - return None - } - }, - _ => todo!() - }) - } - - pub(crate) fn midi_in_add (&mut self) -> Usually<()> { - self.midi_ins.push(JackMidiIn::new(&self.jack, &format!("M/{}", self.midi_ins.len()), &[])?); - Ok(()) - } - - pub(crate) fn midi_out_add (&mut self) -> Usually<()> { - self.midi_outs.push(JackMidiOut::new(&self.jack, &format!("{}/M", self.midi_outs.len()), &[])?); - Ok(()) - } - pub(crate) fn device_kinds (&self) -> &'static [&'static str] { &[ "Sampler", "Plugin (LV2)", ] } - - pub(crate) fn device_picker_show (&mut self) { - self.dialog = Some(Dialog::Device(0)); - } - - pub(crate) fn device_pick (&mut self, index: usize) { - self.dialog = Some(Dialog::Device(index)); - } - pub(crate) fn device_add (&mut self, index: usize) -> Usually<()> { match index { - 0 => self.device_add_sampler(), - 1 => self.device_add_lv2(), + 0 => self.arrangement.device_add_sampler(), + 1 => self.arrangement.device_add_lv2(), _ => unreachable!(), } } - - fn device_add_sampler (&mut self) -> Usually<()> { - let name = self.jack.with_client(|c|c.name().to_string()); - let midi = self.track().expect("no active track").sequencer.midi_outs[0].name(); - let sampler = if let Ok(sampler) = Sampler::new( - &self.jack, - &format!("{}/Sampler", &self.track().expect("no active track").name), - &[PortConnect::exact(format!("{name}:{midi}"))], - &[&[], &[]], - &[&[], &[]] - ) { - self.dialog = None; - Device::Sampler(sampler) - } else { - self.dialog = Some(Dialog::Message(Message::FailedToAddDevice)); - return Err("failed to add device".into()) - }; - self.track_mut().expect("no active track").devices.push(sampler); - Ok(()) - } - - fn device_add_lv2 (&mut self) -> Usually<()> { - todo!(); - Ok(()) - } } +/// Various possible dialog overlays +#[derive(PartialEq, Clone, Copy, Debug)] +pub enum Dialog { + Help, + Menu, + Device(usize), + Message(Message), + Save, + Load, + Options, +} + +/// Various possible messages +#[derive(PartialEq, Clone, Copy, Debug)] +pub enum Message { + FailedToAddDevice, +} + +content!(TuiOut: |self: Message| match self { + Self::FailedToAddDevice => "Failed to add device." +}); + has_size!(|self: App|&self.size); has_clock!(|self: App|self.clock); @@ -458,3 +108,154 @@ has_editor!(|self: App|{ editor_h = 15; is_editing = self.editing.load(Relaxed); }); + +impl HasTracks for App { + fn midi_ins (&self) -> &Vec { + &self.arranger.midi_ins + } + fn midi_outs (&self) -> &Vec { + &self.arranger.midi_outs + } + fn tracks (&self) -> &Vec { + &self.arranger.tracks + } + fn tracks_mut (&mut self) -> &mut Vec { + &mut self.arranger.tracks + } +} + +impl HasScenes for Arrangement { + fn scenes (&self) -> &Vec { + &self.arranger.scenes + } + fn scenes_mut (&mut self) -> &mut Vec { + &mut self.arranger.scenes + } +}if + +#[tengri_proc::expose] +impl App { + fn _todo_isize_stub (&self) -> isize { + todo!() + } + fn _todo_item_theme_stub (&self) -> ItemTheme { + todo!() + } + fn focus_editor (&self) -> bool { + self.is_editing() + } + fn focus_message (&self) -> bool { + matches!(self.dialog, Some(Dialog::Message(..))) + } + fn focus_device_add (&self) -> bool { + matches!(self.dialog, Some(Dialog::Device(..))) + } + fn focus_browser (&self) -> bool { + self.browser.is_visible + } + fn focus_clip (&self) -> bool { + !self.is_editing() && self.selected.is_clip() + } + fn focus_track (&self) -> bool { + !self.is_editing() && self.selected.is_track() + } + fn focus_scene (&self) -> bool { + !self.is_editing() && self.selected.is_scene() + } + fn focus_mix (&self) -> bool { + !self.is_editing() && self.selected.is_mix() + } + fn focus_pool_import (&self) -> bool { + matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Import(..))) + } + fn focus_pool_export (&self) -> bool { + matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Export(..))) + } + fn focus_pool_rename (&self) -> bool { + matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Rename(..))) + } + fn focus_pool_length (&self) -> bool { + matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Length(..))) + } + fn dialog_device (&self) -> Dialog { + Dialog::Device(0) // TODO + } + fn dialog_device_prev (&self) -> Dialog { + Dialog::Device(0) // TODO + } + fn dialog_device_next (&self) -> Dialog { + Dialog::Device(0) // TODO + } + fn dialog_help (&self) -> Dialog { + Dialog::Help + } + fn dialog_menu (&self) -> Dialog { + Dialog::Menu + } + fn dialog_save (&self) -> Dialog { + Dialog::Save + } + fn dialog_load (&self) -> Dialog { + Dialog::Load + } + fn dialog_options (&self) -> Dialog { + Dialog::Options + } + fn editor_pitch (&self) -> Option { + Some((self.editor().map(|e|e.get_note_pos()).unwrap() as u8).into()) + } + fn scene_count (&self) -> usize { + self.scenes.len() + } + fn scene_selected (&self) -> Option { + self.selected.scene() + } + fn track_count (&self) -> usize { + self.tracks.len() + } + fn track_selected (&self) -> Option { + self.selected.track() + } + fn select_scene_next (&self) -> Selection { + self.selected.scene_next(self.scenes.len()) + } + fn select_scene_prev (&self) -> Selection { + self.selected.scene_prev() + } + fn select_track_header (&self) -> Selection { + self.selected.track_header(self.tracks.len()) + } + fn select_track_next (&self) -> Selection { + self.selected.track_next(self.tracks.len()) + } + fn select_track_prev (&self) -> Selection { + self.selected.track_prev() + } + fn clip_selected (&self) -> Option>> { + match self.selected { + Selection::TrackClip { track, scene } => self.scenes[scene].clips[track].clone(), + _ => None + } + } + fn device_kind (&self) -> usize { + if let Some(Dialog::Device(index)) = self.dialog { + index + } else { + 0 + } + } + fn device_kind_prev (&self) -> usize { + if let Some(Dialog::Device(index)) = self.dialog { + index.overflowing_sub(1).0.min(self.device_kinds().len().saturating_sub(1)) + } else { + 0 + } + } + fn device_kind_next (&self) -> usize { + if let Some(Dialog::Device(index)) = self.dialog { + (index + 1) % self.device_kinds().len() + } else { + 0 + } + } +} diff --git a/crates/app/src/model/dialog.rs b/crates/app/src/model/dialog.rs deleted file mode 100644 index c2149659..00000000 --- a/crates/app/src/model/dialog.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::*; - -impl App { - pub fn toggle_dialog (&mut self, dialog: Option) { - self.dialog = if self.dialog == dialog { - None - } else { - dialog - } - } - pub(crate) fn message_dismiss (&mut self) { - self.dialog = None; - } -} - -/// Various possible dialog overlays -#[derive(PartialEq, Clone, Copy, Debug)] -pub enum Dialog { - Help, - Menu, - Device(usize), - Message(Message), - Save, - Load, - Options, -} - -/// Various possible messages -#[derive(PartialEq, Clone, Copy, Debug)] -pub enum Message { - FailedToAddDevice, -} - -content!(TuiOut: |self: Message| match self { - Self::FailedToAddDevice => "Failed to add device." -}); diff --git a/crates/app/src/model/pool.rs b/crates/app/src/model/pool.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/app/src/model/scene.rs b/crates/app/src/model/scene.rs deleted file mode 100644 index 21d4f4ee..00000000 --- a/crates/app/src/model/scene.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::*; - -#[derive(Debug, Default)] pub struct Scene { - /// Name of scene - pub name: Arc, - /// Clips in scene, one per track - pub clips: Vec>>>, - /// Identifying color of scene - pub color: ItemTheme, -} - -impl Scene { - /// Returns the pulse length of the longest clip in the scene - pub fn pulses (&self) -> usize { - self.clips.iter().fold(0, |a, p|{ - a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) - }) - } - /// Returns true if all clips in the scene are - /// currently playing on the given collection of tracks. - pub fn is_playing (&self, tracks: &[Track]) -> bool { - self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() - .all(|(track_index, clip)|match clip { - Some(c) => tracks - .get(track_index) - .map(|track|{ - if let Some((_, Some(clip))) = track.sequencer().play_clip() { - *clip.read().unwrap() == *c.read().unwrap() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) - } - pub fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } - } -} - -pub trait HasScenes: HasSelection + HasEditor + Send + Sync { - fn scenes (&self) -> &Vec; - fn scenes_mut (&mut self) -> &mut Vec; - fn scene_longest (&self) -> usize { - self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) - } - fn scene (&self) -> Option<&Scene> { - self.selected().scene() - .and_then(|s|self.scenes().get(s)) - } - fn scene_mut (&mut self) -> Option<&mut Scene> { - self.selected().scene() - .and_then(|s|self.scenes_mut().get_mut(s)) - } - fn scene_del (&mut self, index: usize) -> Option { - self.selected().scene() - .and_then(|s|Some(self.scenes_mut().remove(index))) - } - /// Set the color of a scene, returning the previous one. - fn scene_set_color (&mut self, index: usize, color: ItemTheme) -> ItemTheme { - let scenes = self.scenes_mut(); - let old = scenes[index].color; - scenes[index].color = color; - old - } - /// Generate the default name for a new scene - fn scene_default_name (&self) -> Arc { - format!("Sc{:3>}", self.scenes().len() + 1).into() - } -} - -impl HasScenes for App { - fn scenes (&self) -> &Vec { &self.scenes } - fn scenes_mut (&mut self) -> &mut Vec { &mut self.scenes } -} diff --git a/crates/app/src/model/track.rs b/crates/app/src/model/track.rs deleted file mode 100644 index a94e5b0f..00000000 --- a/crates/app/src/model/track.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::*; - -#[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, -} - -has_clock!(|self: Track|self.sequencer.clock); - -has_sequencer!(|self: Track|self.sequencer); - -impl Track { - /// Create a new track with only the default [Sequencer]. - pub fn new ( - name: &impl AsRef, - color: Option, - jack: &Jack, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[PortConnect], - midi_to: &[PortConnect], - ) -> Usually { - Ok(Self { - name: name.as_ref().into(), - color: color.unwrap_or_default(), - sequencer: Sequencer::new( - format!("{}/sequencer", name.as_ref()), - jack, - clock, - clip, - midi_from, - midi_to - )?, - ..Default::default() - }) - } - /// Create a new track connecting the [Sequencer] to a [Sampler]. - pub fn new_with_sampler ( - name: &impl AsRef, - color: Option, - jack: &Jack, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[PortConnect], - midi_to: &[PortConnect], - audio_from: &[&[PortConnect];2], - audio_to: &[&[PortConnect];2], - ) -> Usually { - let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; - track.devices.push(Device::Sampler(Sampler::new( - jack, - &format!("{}/sampler", name.as_ref()), - &[PortConnect::exact(format!("{}:{}", - jack.with_client(|c|c.name().to_string()), - track.sequencer.midi_outs[0].name() - ))], - audio_from, - audio_to - )?)); - Ok(track) - } - pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { - for device in self.devices.iter() { - match device { - Device::Sampler(s) => if nth == 0 { - return Some(s); - } else { - nth -= 1; - }, - _ => {} - } - } - None - } - pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { - for device in self.devices.iter_mut() { - match device { - Device::Sampler(s) => if nth == 0 { - return Some(s); - } else { - nth -= 1; - }, - _ => {} - } - } - None - } -} - -pub trait HasWidth { - const MIN_WIDTH: usize; - /// Increment track width. - fn width_inc (&mut self); - /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. - fn width_dec (&mut self); -} - -impl HasWidth for Track { - const MIN_WIDTH: usize = 9; - fn width_inc (&mut self) { - self.width += 1; - } - fn width_dec (&mut self) { - if self.width > Track::MIN_WIDTH { - self.width -= 1; - } - } -} - -pub trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync { - fn midi_ins (&self) -> &Vec; - fn midi_outs (&self) -> &Vec; - fn tracks (&self) -> &Vec; - fn tracks_mut (&mut self) -> &mut Vec; - fn track_longest (&self) -> usize { - self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) - } - const WIDTH_OFFSET: usize = 1; - fn track (&self) -> Option<&Track> { - self.selected().track().and_then(|s|self.tracks().get(s)) - } - fn track_mut (&mut self) -> Option<&mut Track> { - self.selected().track().and_then(|s|self.tracks_mut().get_mut(s)) - } - /// Set the color of a track - fn track_set_color (&mut self, index: usize, color: ItemTheme) -> ItemTheme { - let tracks = self.tracks_mut(); - let old = tracks[index].color; - tracks[index].color = color; - old - } - /// Toggle track recording - fn track_toggle_record (&mut self) { - if let Some(t) = self.selected().track() { - let tracks = self.tracks_mut(); - tracks[t-1].sequencer.recording = !tracks[t-1].sequencer.recording; - } - } - /// Toggle track monitoring - fn track_toggle_monitor (&mut self) { - if let Some(t) = self.selected().track() { - let tracks = self.tracks_mut(); - tracks[t-1].sequencer.monitoring = !tracks[t-1].sequencer.monitoring; - } - } -} - -impl HasTracks for App { - fn midi_ins (&self) -> &Vec { &self.midi_ins } - fn midi_outs (&self) -> &Vec { &self.midi_outs } - fn tracks (&self) -> &Vec { &self.tracks } - fn tracks_mut (&mut self) -> &mut Vec { &mut self.tracks } -} diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index de290bcf..ff8a7c07 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -2,8 +2,12 @@ use crate::*; pub(crate) use std::fmt::Write; pub(crate) use ::tengri::tui::ratatui::prelude::Position; -mod view_dialog; pub use self::view_dialog::*; -mod view_output; pub use self::view_output::*; +impl App { + + pub fn update_clock (&self) { + ViewCache::update_clock(&self.view_cache, self.clock(), self.size.w() > 80) + } +} #[tengri_proc::view(TuiOut)] impl App { @@ -23,7 +27,7 @@ impl App { cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone()) } pub fn view_arranger (&self) -> impl Content + use<'_> { - ArrangerView::new(self) + ArrangerView::new(self, self.editor.as_ref()) } pub fn view_pool (&self) -> impl Content + use<'_> { self.pool().map(|p|Fixed::x(self.w_sidebar(), PoolView(self.is_editing(), p))) @@ -50,453 +54,75 @@ impl App { self.sampler().map(|s|s.view_meters_output()) } pub fn view_dialog (&self) -> impl Content + use<'_> { - view_dialog(self) - } -} - -impl App { - - /// Spacing between tracks. - pub(crate) const TRACK_SPACING: usize = 0; - - /// Default scene height. - pub(crate) const H_SCENE: usize = 2; - - /// Default editor height. - pub(crate) const H_EDITOR: usize = 15; - - pub(crate) fn inputs_with_sizes (&self) -> impl PortsSizes<'_> { - let mut y = 0; - self.midi_ins.iter().enumerate().map(move|(i, input)|{ - let height = 1 + input.conn().len(); - let data = (i, input.name(), input.conn(), y, y + height); - y += height; - data - }) - } - - pub(crate) fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { - use Selection::*; - let mut x = 0; - let editing = self.is_editing(); - let active = match self.selected() { - Track(t) if editing => Some(t), - TrackClip { track, .. } if editing => Some(track), - _ => None - }; - let bigger = self.editor_w(); - self.tracks().iter().enumerate().map(move |(index, track)|{ - let width = if Some(index) == active.copied() { bigger } else { track.width.max(8) }; - let data = (index, track, x, x + width); - x += width + App::TRACK_SPACING; - data - }) - } - - pub(crate) fn scenes_with_sizes (&self, editing: bool, height: usize, larger: usize) - -> impl ScenesSizes<'_> - { - use Selection::*; - let (selected_track, selected_scene) = match self.selected() { - Track(t) => (Some(*t), None), - Scene(s) => (None, Some(*s)), - TrackClip { track, scene } => (Some(*track), Some(*scene)), - _ => (None, None) - }; - let mut y = 0; - self.scenes().iter().enumerate().map(move|(s, scene)|{ - let active = editing && selected_track.is_some() && selected_scene == Some(s); - let height = if active { larger } else { height }; - let data = (s, scene, y, y + height); - y += height; - data - }) - } - - pub fn update_clock (&self) { - ViewCache::update_clock(&self.view_cache, self.clock(), self.size.w() > 80) - } - -} - -pub(crate) struct ArrangerView<'a> { - app: &'a App, - - is_editing: bool, - - width: u16, - width_mid: u16, - width_side: u16, - - inputs_count: usize, - inputs_height: u16, - - outputs_count: usize, - outputs_height: u16, - - scene_last: usize, - scene_count: usize, - scene_scroll: Fill>, - scene_selected: Option, - scenes_height: u16, - - track_scroll: Fill>, - track_count: usize, - track_selected: Option, - tracks_height: u16, - - show_debug_info: bool, -} - -impl<'a> Content for ArrangerView<'a> { - fn content (&self) -> impl Render { - let ins = |x|Bsp::n(self.inputs(), x); - let tracks = |x|Bsp::s(self.tracks(), x); - let devices = |x|Bsp::s(self.devices(), x); - let outs = |x|Bsp::s(self.outputs(), x); - let bg = |x|Tui::bg(Reset, x); - //let track_scroll = |x|Bsp::s(&self.track_scroll, x); - //let scene_scroll = |x|Bsp::e(&self.scene_scroll, x); - outs(tracks(devices(ins(bg(self.scenes()))))) - } -} - -impl<'a> ArrangerView<'a> { - pub fn new (app: &'a App) -> Self { - Self { - app, - is_editing: app.is_editing(), - - width: app.w(), - width_mid: app.w_tracks_area(), - width_side: app.w_sidebar(), - - inputs_height: app.h_inputs(), - inputs_count: app.midi_ins.len(), - - outputs_height: app.h_outputs(), - outputs_count: app.midi_outs.len(), - - scenes_height: app.h_scenes_area(), - scene_selected: app.selected().scene(), - scene_count: app.scenes.len(), - scene_last: app.scenes.len().saturating_sub(1), - scene_scroll: Fill::y(Fixed::x(1, ScrollbarV { - offset: app.scene_scroll, - length: app.h_scenes_area() as usize, - total: app.h_scenes() as usize, - })), - - tracks_height: app.h_tracks_area(), - track_count: app.tracks.len(), - track_selected: app.selected().track(), - track_scroll: Fill::x(Fixed::y(1, ScrollbarH { - offset: app.track_scroll, - length: app.h_tracks_area() as usize, - total: app.h_scenes() as usize, - })), - - show_debug_info: false - } - } - - /// Render input matrix. - pub(crate) fn inputs (&'a self) -> impl Content + 'a { - Tui::bg(Reset, Bsp::s( - self.input_intos(), - Bsp::s(self.input_routes(), self.input_ports()), + When(app.dialog.is_some(), Bsp::b( "", + Fixed::xy(70, 23, Tui::fg_bg(Rgb(255,255,255), Rgb(16,16,16), Bsp::b( + Repeat(" "), Outer(true, Style::default().fg(Tui::g(96))) + .enclose(app.dialog.as_ref().map(|dialog|match dialog { + Dialog::Menu => + app.view_dialog_menu().boxed(), + Dialog::Help => + app.view_dialog_help().boxed(), + Dialog::Save => + app.view_dialog_save().boxed(), + Dialog::Load => + app.view_dialog_load().boxed(), + Dialog::Options => + app.view_dialog_options().boxed(), + Dialog::Device(index) => + app.view_dialog_device(*index).boxed(), + Dialog::Message(message) => + app.view_dialog_message(message).boxed(), + })) + ))) )) } - - /// Render device switches. - pub(crate) fn devices (&'a self) -> impl Content + 'a { - let Self { width_side, width_mid, track_count, track_selected, is_editing, .. } = self; - Tryptich::top(1) - .left(*width_side, button_3("d", "devices", format!("{}", 0), *is_editing)) - .right(*width_side, button_2("D", "add device", *is_editing)) - .middle(*width_mid, per_track_top(*width_mid, ||self.tracks_with_sizes_scrolled(), - move|index, track|{ - let bg = if *track_selected == Some(index) { - track.color.light - } else { - track.color.base - }; - let fg = Tui::g(224); - track.devices.get(0).map(|device|wrap(bg.rgb, fg, device.name())) - })) + pub fn view_dialog_menu (&self) -> impl Content { + let options = ||["Projects", "Settings", "Help", "Quit"].iter(); + let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); + Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) } - - /// Render track headers - pub(crate) fn tracks (&'a self) -> impl Content + 'a { - let Self { width_side, width_mid, track_count, track_selected, is_editing, .. } = self; - Tryptich::center(3) - .left(*width_side, button_3("t", "track", format!("{}", *track_count), *is_editing)) - .right(*width_side, button_2("T", "add track", *is_editing)) - .middle(*width_mid, per_track(*width_mid, ||self.tracks_with_sizes_scrolled(), - |index, track|wrap( - if *track_selected == Some(index) { - track.color.light - } else { - track.color.base - }.rgb, - track.color.lightest.rgb, - Tui::bold(true, Fill::xy(Align::nw(&track.name))) - ))) + pub fn view_dialog_help <'a> (&'a self) -> impl Content + use<'a> { + Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1, + ||self.config.keys.layers.iter() + .filter_map(|a|(a.0)(self).then_some(a.1)) + .flat_map(|a|a) + .filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None }) + .take(20), + |mut b,i|Fixed::x(60, Align::w(Bsp::e("(", Bsp::e( + b.next().map(|t|Fixed::x(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), + Bsp::e(" ", Align::w(format!("{}", b.0.0.trim())))))))))) } - - fn input_routes (&'a self) -> impl Content + 'a { - Tryptich::top(self.inputs_height) - .left(self.width_side, - io_ports(Tui::g(224), Tui::g(32), ||self.app.inputs_with_sizes())) - .middle(self.width_mid, - per_track_top(self.width_mid, ||self.tracks_with_sizes_scrolled(), - move|_, &Track { color, .. }|io_conns( - color.dark.rgb, - color.darker.rgb, - ||self.app.inputs_with_sizes() - ))) + pub fn view_dialog_device (&self, index: usize) -> impl Content + use<'_> { + let choices = ||self.device_kinds().iter(); + let choice = move|label, i| + Fill::x(Tui::bg(if i == index { Rgb(64,128,32) } else { Rgb(0,0,0) }, + Bsp::e(if i == index { "[ " } else { " " }, + Bsp::w(if i == index { " ]" } else { " " }, + label)))); + Bsp::s(Tui::bold(true, "Add device"), Map::south(1, choices, choice)) } - - fn input_ports (&'a self) -> impl Content + 'a { - Tryptich::top(1) - .left(self.width_side, - button_3("i", "midi ins", format!("{}", self.inputs_count), self.is_editing)) - .right(self.width_side, - button_2("I", "add midi in", self.is_editing)) - .middle(self.width_mid, - per_track_top( - self.width_mid, - ||self.tracks_with_sizes_scrolled(), - move|t, track|{ - let rec = track.sequencer.recording; - let mon = track.sequencer.monitoring; - let rec = if rec { White } else { track.color.darkest.rgb }; - let mon = if mon { White } else { track.color.darkest.rgb }; - let bg = if self.track_selected == Some(t) { - track.color.light.rgb - } else { - track.color.base.rgb - }; - //let bg2 = if t > 0 { track.color.base.rgb } else { Reset }; - wrap(bg, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e( - Tui::fg_bg(rec, bg, "Rec "), - Tui::fg_bg(mon, bg, "Mon "), - )))) - })) + pub fn view_dialog_message <'a> (&'a self, message: &'a Message) -> impl Content + use<'a> { + Bsp::s(message, Bsp::s("", "[ OK ]")) } - - fn input_intos (&'a self) -> impl Content + 'a { - Tryptich::top(2) - .left(self.width_side, - Bsp::s(Align::e("Input:"), Align::e("Into clip:"))) - .middle(self.width_mid, - per_track_top( - self.width_mid, - ||self.tracks_with_sizes_scrolled(), - |_, _|Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ "))))) + pub fn view_dialog_save <'a> (&'a self) -> impl Content + use<'a> { + Bsp::s( + Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( + Tui::bold(true, " Save project: "), + Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::xy("todo file browser"))) } - - /// Render scenes with clips - pub(crate) fn scenes (&'a self) -> impl Content + 'a { - - let Self { - width, width_side, width_mid, - scenes_height, scene_last, scene_selected, - track_selected, is_editing, app: App { editor, .. }, .. - } = self; - - let scene_headers = Map::new(||self.scenes_with_scene_colors(), - move|(s, scene, y1, y2, previous): SceneWithColor, _|{ - let height = (1 + y2 - y1) as u16; - let name = Some(scene.name.clone()); - let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⯈ ", name)))); - let same_track = true; - let selected = same_track && *scene_selected == Some(s); - let neighbor = same_track && s > 0 && *scene_selected == Some(s - 1); - let is_last = *scene_last == s; - let theme = scene.color; - let fg = theme.lightest.rgb; - let bg = if selected { theme.light } else { theme.base }.rgb; - let hi = if let Some(previous) = previous { - if neighbor { - previous.light.rgb - } else { - previous.base.rgb - } - } else { - Reset - }; - let lo = if is_last { - Reset - } else if selected { - theme.light.rgb - } else { - theme.base.rgb - }; - Fill::x(map_south(y1 as u16, height, Fixed::y(height, Phat { - width: 0, height: 0, content, colors: [fg, bg, hi, lo] - }))) - }); - - let scene_track_clips = per_track(*width_mid, ||self.tracks_with_sizes_scrolled(), - move|track_index, track|Map::new(move||self.scenes_with_track_colors(track_index), - move|(s, scene, y1, y2, previous): SceneWithColor<'a>, _|{ - let (name, theme) = if let Some(clip) = &scene.clips[track_index] { - let clip = clip.read().unwrap(); - (Some(clip.name.clone()), clip.color) - } else { - (None, ItemTheme::G[32]) - }; - let height = (1 + y2 - y1) as u16; - let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⏹ ", name)))); - let same_track = *track_selected == Some(track_index); - let selected = same_track && *scene_selected == Some(s); - let neighbor = same_track && s > 0 && *scene_selected == Some(s - 1); - let is_last = *scene_last == s; - let fg = theme.lightest.rgb; - let bg = if selected { theme.light } else { theme.base }.rgb; - let hi = if let Some(previous) = previous { - if neighbor { - previous.light.rgb - } else { - previous.base.rgb - } - } else { - Reset - }; - let lo = if is_last { - Reset - } else if selected { - theme.light.rgb - } else { - theme.base.rgb - }; - map_south(y1 as u16, height, Bsp::b(Fixed::y(height, Phat { - width: 0, height: 0, content, colors: [fg, bg, hi, lo] - }), When( - *is_editing && same_track && *scene_selected == Some(s), - editor - ))) - })); - - Tryptich::center(*scenes_height) - .left(*width_side, scene_headers) - .middle(*width_mid, scene_track_clips) + pub fn view_dialog_load <'a> (&'a self) -> impl Content + use<'a> { + Bsp::s( + Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( + Tui::bold(true, " Load project: "), + Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::xy("todo file browser"))) } - - pub(crate) fn tracks_with_sizes_scrolled (&'a self) - -> impl TracksSizes<'a> - { - let width = self.width_mid; - self.app.tracks_with_sizes().map_while(move|(t, track, x1, x2)|{ - (width > x2 as u16).then_some((t, track, x1, x2)) - }) + pub fn view_dialog_options <'a> (&'a self) -> impl Content + use<'a> { + "TODO" } - - pub(crate) fn scenes_with_scene_colors (&self) - -> impl ScenesColors<'_> - { - self.app.scenes_with_sizes(self.is_editing, App::H_SCENE, App::H_EDITOR).map_while( - move|(s, scene, y1, y2)|if y2 as u16 > self.scenes_height { - None - } else { - Some((s, scene, y1, y2, if s == 0 { - None - } else { - Some(self.app.scenes()[s-1].color) - })) - }) - } - - pub(crate) fn scenes_with_track_colors (&self, track: usize) - -> impl ScenesColors<'_> - { - self.app.scenes_with_sizes(self.is_editing, App::H_SCENE, App::H_EDITOR).map_while( - move|(s, scene, y1, y2)|if y2 as u16 > self.scenes_height { - None - } else { - Some((s, scene, y1, y2, if s == 0 { - None - } else { - Some(self.app.scenes[s-1].clips[track].as_ref() - .map(|c|c.read().unwrap().color) - .unwrap_or(ItemTheme::G[32])) - })) - } - ) - } - -} - -/// Iterator over scenes with their sizes and colors. -pub(crate) trait ScenesColors<'a> = - Iterator>; - -/// A scene with size and color. -pub(crate) type SceneWithColor<'a> = - (usize, &'a Scene, usize, usize, Option); - -/// Define a type alias for iterators of sized items (columns). -macro_rules! def_sizes_iter { - ($Type:ident => $($Item:ty),+) => { - pub(crate) trait $Type<'a> = - Iterator + Send + Sync + 'a; - } -} - -def_sizes_iter!(ScenesSizes => Scene); - -def_sizes_iter!(TracksSizes => Track); - -def_sizes_iter!(InputsSizes => JackMidiIn); - -def_sizes_iter!(OutputsSizes => JackMidiOut); - -def_sizes_iter!(PortsSizes => Arc, [PortConnect]); - -pub(crate) fn wrap (bg: Color, fg: Color, content: impl Content) -> impl Content { - let left = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV("▐"))); - let right = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV("▌"))); - Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content))) -} - -pub(crate) fn button_2 <'a> ( - key: impl Content + 'a, label: impl Content + 'a, editing: bool, -) -> impl Content + 'a { - let key = Tui::fg_bg(Tui::g(0), Tui::orange(), Bsp::e( - Tui::fg_bg(Tui::orange(), Reset, "▐"), - Bsp::e(key, Tui::fg(Tui::g(96), "▐")) - )); - let label = When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label)); - Tui::bold(true, Bsp::e(key, label)) -} - -pub(crate) fn button_3 <'a, K, L, V> ( - key: K, - label: L, - value: V, - editing: bool, -) -> impl Content + 'a where - K: Content + 'a, - L: Content + 'a, - V: Content + 'a, -{ - let key = Tui::fg_bg(Tui::g(0), Tui::orange(), - Bsp::e(Tui::fg_bg(Tui::orange(), Reset, "▐"), Bsp::e(key, Tui::fg(if editing { - Tui::g(128) - } else { - Tui::g(96) - }, "▐")))); - let label = Bsp::e( - When::new(!editing, Bsp::e( - Tui::fg_bg(Tui::g(255), Tui::g(96), label), - Tui::fg_bg(Tui::g(128), Tui::g(96), "▐"), - )), - Bsp::e( - Tui::fg_bg(Tui::g(224), Tui::g(128), value), - Tui::fg_bg(Tui::g(128), Reset, "▌"), - )); - Tui::bold(true, Bsp::e(key, label)) } pub(crate) fn heading <'a> ( @@ -510,57 +136,6 @@ pub(crate) fn heading <'a> ( Fill::xy(Align::w(Bsp::s(Fill::x(Align::w(button_3(key, label, count, editing))), content))) } -pub(crate) fn io_ports <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a -) -> impl Content + 'a { - Map::new(iter, move|( - index, name, connections, y, y2 - ): (usize, &'a Arc, &'a [PortConnect], usize, usize), _| - map_south(y as u16, (y2-y) as u16, Bsp::s( - Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(" 󰣲 ", name))))), - Map::new(||connections.iter(), move|connect: &'a PortConnect, index|map_south(index as u16, 1, - Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, - &connect.info))))))))) -} - -pub(crate) fn io_conns <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a -) -> impl Content + 'a { - Map::new(iter, move|( - index, name, connections, y, y2 - ): (usize, &'a Arc, &'a [PortConnect], usize, usize), _| - map_south(y as u16, (y2-y) as u16, Bsp::s( - Fill::x(Tui::bold(true, wrap(bg, fg, Fill::x(Align::w("▞▞▞▞ ▞▞▞▞"))))), - Map::new(||connections.iter(), move|connect, index|map_south(index as u16, 1, - Fill::x(Align::w(Tui::bold(false, wrap(bg, fg, Fill::x("")))))))))) -} - -pub(crate) fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( - width: u16, - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Content + 'a { - Align::x(Tui::bg(Reset, Map::new(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - map_east(x1 as u16, width, Fixed::x(width, Tui::fg_bg( - track.color.lightest.rgb, - track.color.base.rgb, - callback(index, track))))}))) -} - -pub(crate) fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( - width: u16, - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Content + 'a { - per_track_top( - width, - tracks, - move|index, track|Fill::y(Align::y(callback(index, track))) - ) -} - /// Clear a pre-allocated buffer, then write into it. #[macro_export] macro_rules! rewrite { ($buf:ident, $($rest:tt)*) => { |$buf,_,_|{ $buf.clear(); write!($buf, $($rest)*) } } diff --git a/crates/app/src/view/view_dialog.rs b/crates/app/src/view/view_dialog.rs deleted file mode 100644 index 5ab8f1ef..00000000 --- a/crates/app/src/view/view_dialog.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::*; - -pub(crate) fn view_dialog (app: &App) -> impl Content + use<'_> { - When::new(app.dialog.is_some(), Bsp::b( - "", - Fixed::xy(70, 23, Tui::fg_bg(Rgb(255,255,255), Rgb(16,16,16), Bsp::b( - Repeat(" "), - Outer(true, Style::default().fg(Tui::g(96))) - .enclose(app.dialog.as_ref().map(|dialog|match dialog { - Dialog::Menu => app.view_dialog_menu().boxed(), - Dialog::Help => app.view_dialog_help().boxed(), - Dialog::Save => app.view_dialog_save().boxed(), - Dialog::Load => app.view_dialog_load().boxed(), - Dialog::Options => app.view_dialog_options().boxed(), - Dialog::Device(index) => app.view_dialog_device(*index).boxed(), - Dialog::Message(message) => app.view_dialog_message(message).boxed(), - })) - ))) - )) -} - -impl App { - pub fn view_dialog_menu (&self) -> impl Content { - let options = ||["Projects", "Settings", "Help", "Quit"].iter(); - let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); - Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) - } - pub fn view_dialog_help <'a> (&'a self) -> impl Content + use<'a> { - //let bindings = ; - //let binding = ;[> Bsp::e( - //Fixed::x(15, Align::w(Tui::bold(true, Tui::fg(Rgb(255,192,0), if let Some(Token { - //value: Value::Sym(key), .. - //}) = binding.next() { - //Some(key.to_string()) - //} else { - //None - //})))), - //Bsp::e(" ", Tui::fg(Rgb(255,255,255), if let Some(Token { - //value: Value::Key(command), .. - //}) = binding.next() { - //Some(command.to_string()) - //} else { - //None - //})), - //);*/ - Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1, - ||self.config.keys.layers.iter() - .filter_map(|a|(a.0)(self).then_some(a.1)) - .flat_map(|a|a) - .filter_map(|x|if let Value::Exp(_, iter)=x.value{ - Some(iter) - } else { - None - }) - .take(20), - |mut b,i|Bsp::e( - Min::x(30, Max::x(60, format!("?"))), - b.next().map(|t|Min::x(16, Tui::fg(Rgb(224,64,0), format!("{}", t.value)))), - )))) - //format!("{b:?}"))))) - //|mut binding: TokenIter, _|Map::east(5, move||binding.clone(), |_,_|"kyp")))) - } - - pub fn view_dialog_device (&self, index: usize) -> impl Content + use<'_> { - let choices = ||self.device_kinds().iter(); - let choice = move|label, i| - Fill::x(Tui::bg(if i == index { Rgb(64,128,32) } else { Rgb(0,0,0) }, - Bsp::e(if i == index { "[ " } else { " " }, - Bsp::w(if i == index { " ]" } else { " " }, - label)))); - Bsp::s(Tui::bold(true, "Add device"), Map::south(1, choices, choice)) - } - - pub fn view_dialog_message <'a> (&'a self, message: &'a Message) -> impl Content + use<'a> { - Bsp::s(message, Bsp::s("", "[ OK ]")) - } - - pub fn view_dialog_save <'a> (&'a self) -> impl Content + use<'a> { - Bsp::s( - Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - Tui::bold(true, " Save project: "), - Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), - Outer(true, Style::default().fg(Tui::g(96))) - .enclose(Fill::xy("todo file browser"))) - } - - pub fn view_dialog_load <'a> (&'a self) -> impl Content + use<'a> { - Bsp::s( - Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - Tui::bold(true, " Load project: "), - Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), - Outer(true, Style::default().fg(Tui::g(96))) - .enclose(Fill::xy("todo file browser"))) - } - - pub fn view_dialog_options <'a> (&'a self) -> impl Content + use<'a> { - "TODO" - } -} diff --git a/crates/app/src/view/view_output.rs b/crates/app/src/view/view_output.rs deleted file mode 100644 index 938b90fb..00000000 --- a/crates/app/src/view/view_output.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::*; - -/// Outputs. -impl App { - pub(crate) fn outputs_with_sizes (&self) -> impl PortsSizes<'_> { - let mut y = 0; - self.midi_outs.iter().enumerate().map(move|(i, output)|{ - let height = 1 + output.conn().len(); - let data = (i, output.name(), output.conn(), y, y + height); - y += height; - data - }) - } -} -impl<'a> ArrangerView<'a> { - fn output_nexts (&self) -> impl Content + '_ { - Tryptich::top(2) - .left(self.width_side, Align::ne("From clip:")) - .middle(self.width_mid, per_track_top( - self.width_mid, - ||self.tracks_with_sizes_scrolled(), - |_, _|Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default()))))) - } - /// Render output matrix. - pub(crate) fn outputs (&'a self) -> impl Content + 'a { - Tui::bg(Reset, Align::n(Bsp::s( - Bsp::s(self.output_ports(), self.output_conns()), - Bsp::s(self.output_nexts(), self.output_froms()), - ))) - } - fn output_froms (&'a self) -> impl Content + 'a { - let label = Align::ne("Next clip:"); - Tryptich::top(2).left(self.width_side, label).middle(self.width_mid, per_track_top( - self.width_mid, ||self.tracks_with_sizes_scrolled(), |t, track|{ - let queued = track.sequencer.next_clip.is_some(); - let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ ")); - let queued_clip = Thunk::new(||{ - Tui::bg(Reset, if let Some((_, clip)) = track.sequencer.next_clip.as_ref() { - if let Some(clip) = clip { - clip.read().unwrap().name.clone() - } else { - "Stop".into() - } - } else { - "".into() - }) - }); - Either(queued, queued_clip, queued_blank) - })) - } - fn output_ports (&'a self) -> impl Content + 'a { - Tryptich::top(1) - .left(self.width_side, self.output_count()) - .right(self.width_side, self.output_add()) - .middle(self.width_mid, self.output_map()) - } - fn output_count (&'a self) -> impl Content + 'a { - button_3("o", "midi outs", format!("{}", self.outputs_count), self.is_editing) - } - fn output_add (&'a self) -> impl Content + 'a { - button_2("O", "add midi out", self.is_editing) - } - fn output_map (&'a self) -> impl Content + 'a { - per_track_top(self.width_mid, ||self.tracks_with_sizes_scrolled(), move|i, t|{ - let mute = false; - let solo = false; - let mute = if mute { White } else { t.color.darkest.rgb }; - let solo = if solo { White } else { t.color.darkest.rgb }; - let bg_1 = if self.track_selected == Some(i) { - t.color.light.rgb - } else { - t.color.base.rgb - }; - let bg_2 = if i > 0 { t.color.base.rgb } else { Reset }; - let mute = Tui::fg_bg(mute, bg_1, "Play "); - let solo = Tui::fg_bg(solo, bg_1, "Solo "); - wrap(bg_1, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(mute, solo)))) - }) - } - fn output_conns (&'a self) -> impl Content + 'a { - Tryptich::top(self.outputs_height) - .left(self.width_side, io_ports(Tui::g(224), Tui::g(32), ||self.app.outputs_with_sizes())) - .middle(self.width_mid, per_track_top( - self.width_mid, - ||self.tracks_with_sizes_scrolled(), - |_, t|io_conns( - t.color.dark.rgb, - t.color.darker.rgb, - ||self.app.outputs_with_sizes() - ))) - } -} diff --git a/crates/cli/tek.rs b/crates/cli/tek.rs index 3f7fdfcb..c1954cb7 100644 --- a/crates/cli/tek.rs +++ b/crates/cli/tek.rs @@ -91,6 +91,21 @@ impl Cli { _ => None, }; let scenes = vec![]; + let mut tracks = vec![]; + match mode { + LaunchMode::Sequencer => tracks.push(Track::new( + &name, None, jack, + Some(&clock), clip.as_ref(), + midi_froms.as_slice(), midi_tos.as_slice() + )?), + LaunchMode::Groovebox | LaunchMode::Sampler => tracks.push(Track::new_with_sampler( + &name, None, jack, + Some(&clock), clip.as_ref(), + midi_froms.as_slice(), midi_tos.as_slice(), + audio_froms, audio_tos, + )?), + _ => {} + } Tui::new()?.run(&Jack::new(name)?.run(|jack|{ let mut midi_ins = vec![]; let mut midi_outs = vec![]; @@ -137,33 +152,7 @@ impl Cli { vec![vec![];65536], _ => todo!("{mode:?}"), }, - tracks: match mode { - LaunchMode::Sequencer => vec![ - Track::new( - &name, - None, - jack, - Some(&clock), - clip.as_ref(), - midi_froms.as_slice(), - midi_tos.as_slice() - )? - ], - LaunchMode::Groovebox | LaunchMode::Sampler => vec![ - Track::new_with_sampler( - &name, - None, - jack, - Some(&clock), - clip.as_ref(), - midi_froms.as_slice(), - midi_tos.as_slice(), - audio_froms, - audio_tos, - )? - ], - _ => vec![] - }, + tracks, scenes, selected: Selection::TrackClip { track: 0, scene: 0 }, config, diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml index eff1dea7..6b702f98 100644 --- a/crates/device/Cargo.toml +++ b/crates/device/Cargo.toml @@ -16,8 +16,9 @@ wavers = { workspace = true, optional = true } winit = { workspace = true, optional = true } [features] -default = [ "browser", "clock", "editor", "sequencer", "sampler", "lv2" ] +default = [ "browser", "clock", "editor", "sequencer", "sampler", "lv2", "arranger" ] clock = [] +arranger = [ "editor" ] editor = [] meter = [] mixer = [] diff --git a/crates/device/src/arranger.rs b/crates/device/src/arranger.rs new file mode 100644 index 00000000..396aebe7 --- /dev/null +++ b/crates/device/src/arranger.rs @@ -0,0 +1,86 @@ +use crate::*; + +/// Define a type alias for iterators of sized items (columns). +macro_rules! def_sizes_iter { + ($Type:ident => $($Item:ty),+) => { + pub(crate) trait $Type<'a> = + Iterator + Send + Sync + 'a; + } +} + +mod arranger_api; pub use self::arranger_api::*; +mod arranger_clip; pub use self::arranger_clip::*; +mod arranger_device; pub use self::arranger_device::*; +mod arranger_port; pub use self::arranger_port::*; +mod arranger_model; pub use self::arranger_model::*; +mod arranger_scene; pub use self::arranger_scene::*; +mod arranger_select; pub use self::arranger_select::*; +mod arranger_track; pub use self::arranger_track::*; +mod arranger_view; pub use self::arranger_view::*; + +def_sizes_iter!(ScenesSizes => Scene); +def_sizes_iter!(TracksSizes => Track); +def_sizes_iter!(InputsSizes => JackMidiIn); +def_sizes_iter!(OutputsSizes => JackMidiOut); +def_sizes_iter!(PortsSizes => Arc, [PortConnect]); + +pub(crate) fn wrap (bg: Color, fg: Color, content: impl Content) -> impl Content { + let left = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV("▐"))); + let right = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV("▌"))); + Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content))) +} + +pub(crate) fn io_ports <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a +) -> impl Content + 'a { + Map::new(iter, move|( + index, name, connections, y, y2 + ): (usize, &'a Arc, &'a [PortConnect], usize, usize), _| + map_south(y as u16, (y2-y) as u16, Bsp::s( + Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(" 󰣲 ", name))))), + Map::new(||connections.iter(), move|connect: &'a PortConnect, index|map_south(index as u16, 1, + Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, + &connect.info))))))))) +} + +pub(crate) fn io_conns <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a +) -> impl Content + 'a { + Map::new(iter, move|( + index, name, connections, y, y2 + ): (usize, &'a Arc, &'a [PortConnect], usize, usize), _| + map_south(y as u16, (y2-y) as u16, Bsp::s( + Fill::x(Tui::bold(true, wrap(bg, fg, Fill::x(Align::w("▞▞▞▞ ▞▞▞▞"))))), + Map::new(||connections.iter(), move|connect, index|map_south(index as u16, 1, + Fill::x(Align::w(Tui::bold(false, wrap(bg, fg, Fill::x("")))))))))) +} + +pub 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); +} + +impl HasWidth for Track { + const MIN_WIDTH: usize = 9; + fn width_inc (&mut self) { + self.width += 1; + } + fn width_dec (&mut self) { + if self.width > Track::MIN_WIDTH { + self.width -= 1; + } + } +} + +pub trait Has: Send + Sync { + fn get (&self) -> &T; + fn get_mut (&mut self) -> &mut T; +} + +pub trait HasMany: Send + Sync { + fn get (&self, key: U) -> &T; + fn get_mut (&mut self, key: U) -> &mut T; +} diff --git a/crates/device/src/arranger/arranger_api.rs b/crates/device/src/arranger/arranger_api.rs new file mode 100644 index 00000000..d13d3885 --- /dev/null +++ b/crates/device/src/arranger/arranger_api.rs @@ -0,0 +1,207 @@ +use crate::*; + +#[tengri_proc::command(Arrangement)] +impl ArrangementCommand { + fn track (arranger: &mut Arrangement, track: TrackCommand) -> Perhaps { + todo!("delegate") + } + fn track_add (arranger: &mut Arrangement) -> Perhaps { + let index = arranger.track_add(None, None, &[], &[])?.0; + arranger.selected = match arranger.selected { + Some(Selection::Track(_)) => + Selection::Track(index), + Some(Selection::TrackClip { track, scene }) => + Selection::TrackClip { track: index, scene }, + _ => arranger.selected + }; + Ok(Some(Self::TrackDelete { index })) + } + fn track_swap (arranger: &mut Arrangement, index: usize, other: usize) -> Perhaps { + todo!(); + Ok(Some(Self::TrackSwap { index, other })) + } + fn track_delete (arranger: &mut Arrangement, index: usize) -> Perhaps { + let exists = arranger.tracks().get(index).is_some(); + if exists { + let track = arranger.tracks_mut().remove(index); + let Track { sequencer: Sequencer { midi_ins, midi_outs, .. }, .. } = track; + for port in midi_ins.into_iter() { + port.close()?; + } + for port in midi_outs.into_iter() { + port.close()?; + } + for scene in arranger.scenes_mut().iter_mut() { + scene.clips.remove(index); + } + } + Ok(None) + //TODO:Ok(Some(Self::TrackAdd ( index, track: Some(deleted_track) }) + } + fn midi_in (arranger: &mut Arrangement, input: InputCommand) -> Perhaps { + todo!("delegate") + } + fn midi_in_add (arranger: &mut Arrangement) -> Perhaps { + arranger.midi_in_add()?; + Ok(None) + } + fn midi_out (arranger: &mut Arrangement, input: OutputCommand) -> Perhaps { + todo!("delegate") + } + fn midi_out_add (arranger: &mut Arrangement) -> Perhaps { + arranger.midi_out_add()?; + Ok(None) + } + fn device (arranger: &mut Arrangement, input: DeviceCommand) -> Perhaps { + todo!("delegate") + } + fn device_add (arranger: &mut Arrangement, i: usize) -> Perhaps { + arranger.device_add(i); + Ok(None) + } + fn scene (arranger: &mut Arrangement, scene: SceneCommand) -> Perhaps { + todo!("delegate") + } + fn scene_add (arranger: &mut Arrangement) -> Perhaps { + let index = arranger.scene_add(None, None)?.0; + arranger.selected = match arranger.selected { + Selection::Scene(_) => + Selection::Scene(index), + Selection::TrackClip { track, scene } => + Selection::TrackClip { track, scene: index }, + _ => arranger.selected + }; + Ok(None) // TODO + } + fn scene_swap (arranger: &mut Arrangement, index: usize, other: usize) -> Perhaps { + todo!(); + Ok(Some(Self::SceneSwap { index, other })) + } + fn scene_delete (arranger: &mut Arrangement, index: usize) -> Perhaps { + let scenes = arranger.scenes_mut(); + Ok(if scenes.get(index).is_some() { + Some(scenes.remove(index)) + } else { + None + }) + } + fn scene_launch (arranger: &mut Arrangement, index: usize) -> Perhaps { + for track in 0..arranger.tracks.len() { + let clip = arranger.scenes[index].clips[track].as_ref(); + arranger.tracks[track].sequencer.enqueue_next(clip); + } + } + fn clip (arranger: &mut Arrangement, scene: ClipCommand) -> Perhaps { + todo!("delegate") + } + fn clip_get (arranger: &mut Arrangement, a: usize, b: usize) -> Perhaps { + //(Get [a: usize, b: usize] cmd_todo!("\n\rtodo: clip: get: {a} {b}")) + //("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap()))) + todo!() + } + fn clip_put (arranger: &mut Arrangement, a: usize, b: usize) -> Perhaps { + //(Put [t: usize, s: usize, c: MaybeClip] + //Some(Self::Put(t, s, arranger.clip_put(t, s, c)))) + //("put" [a: usize, b: usize, c: MaybeClip] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap()))) + todo!() + } + fn clip_del (arranger: &mut Arrangement, a: usize, b: usize) -> Perhaps { + //("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None)))) + todo!() + } + fn clip_enqueue (arranger: &mut Arrangement, a: usize, b: usize) -> Perhaps { + //(Enqueue [t: usize, s: usize] + //cmd!(arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref()))) + //("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap()))) + todo!() + } + fn clip_edit (arranger: &mut Arrangement, a: usize, b: usize) -> Perhaps { + //(Edit [clip: MaybeClip] cmd_todo!("\n\rtodo: clip: edit: {clip:?}")) + //("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap()))) + todo!() + } + +} + +#[tengri_proc::command(MidiClip)] +impl ClipCommand { + fn set_color (clip: &mut MidiClip, color: Option) -> Perhaps { + //(SetColor [t: usize, s: usize, c: ItemTheme] + //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); + //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) + todo!() + } + fn set_loop (clip: &mut MidiClip, looping: Option) -> Perhaps { + //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) + //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) + todo!() + } +} + +#[tengri_proc::command(Device)] +impl DeviceCommand { +} + +#[tengri_proc::command(JackMidiIn)] +impl InputCommand { +} + +#[tengri_proc::command(JackMidiOut)] +impl OutputCommand { +} + +#[tengri_proc::command(Scene)] +impl SceneCommand { + fn set_name (scene: &mut Scene, mut name: Arc) -> Perhaps { + std::mem::swap(&mut scene.name, &mut name); + Ok(Some(Self::SetName { name })) + } + fn set_color (scene: &mut Scene, mut color: ItemTheme) -> Perhaps { + std::mem::swap(&mut scene.color, &mut color); + Ok(Some(Self::SetColor { color })) + } + fn set_size (scene: &mut Scene, size: usize) -> Perhaps { + todo!() + } + fn set_zoom (scene: &mut Scene, zoom: usize) -> Perhaps { + todo!() + } +} + +#[tengri_proc::command(Track)] +impl TrackCommand { + fn set_name (track: &mut Track, mut name: Arc) -> Perhaps { + std::mem::swap(&mut name, &mut track.name); + Ok(Some(Self::SetName { name })) + } + fn set_color (track: &mut Track, color: ItemTheme) -> Perhaps { + std::mem::swap(color, track.color); + Ok(Some(Self::SetColor { color })) + } + fn set_mute (track: &mut Track, value: Option) -> Perhaps { + todo!() + } + fn set_solo (track: &mut Track, value: Option) -> Perhaps { + todo!() + } + fn set_rec (track: &mut Track, value: Option) -> Perhaps { + let current = track.sequencer.recording; + let value = value.unwrap_or(!current); + Ok((value != current).then_some(Self::SetRec { value: Some(current) })) + } + fn set_mon (track: &mut Track, value: Option) -> Perhaps { + let current = track.sequencer.monitoring; + let value = value.unwrap_or(!current); + Ok((value != current).then_some(Self::SetRec { value: Some(current) })) + } + fn set_size (track: &mut Track, size: usize) -> Perhaps { + todo!() + } + fn set_zoom (track: &mut Track, zoom: usize) -> Perhaps { + todo!() + } + fn stop (track: &mut Track) -> Perhaps { + track.sequencer.enqueue_next(None); + Ok(None) + } +} diff --git a/crates/device/src/arranger/arranger_audio.rs b/crates/device/src/arranger/arranger_audio.rs new file mode 100644 index 00000000..71139cf5 --- /dev/null +++ b/crates/device/src/arranger/arranger_audio.rs @@ -0,0 +1,7 @@ +use crate::*; + +pub trait RenderTracks { +} + +impl Audio for T { +} diff --git a/crates/device/src/arranger/arranger_clip.rs b/crates/device/src/arranger/arranger_clip.rs new file mode 100644 index 00000000..ba5fe2bc --- /dev/null +++ b/crates/device/src/arranger/arranger_clip.rs @@ -0,0 +1,35 @@ +use crate::*; + + +impl Arrangement { + /// Put a clip in a slot + pub(crate) fn clip_put ( + &mut self, track: usize, scene: usize, clip: Option>> + ) -> Option>> { + let old = self.scenes[scene].clips[track].clone(); + self.scenes[scene].clips[track] = clip; + old + } + /// Change the color of a clip, returning the previous one + pub(crate) fn clip_set_color ( + &self, track: usize, scene: usize, color: ItemTheme + ) -> Option { + self.scenes[scene].clips[track].as_ref().map(|clip|{ + let mut clip = clip.write().unwrap(); + let old = clip.color.clone(); + clip.color = color.clone(); + panic!("{color:?} {old:?}"); + old + }) + } + /// Get the active clip + pub(crate) fn clip (&self) -> Option>> { + self.scene()?.clips.get(self.selected().track()?)?.clone() + } + /// Toggle looping for the active clip + pub(crate) fn toggle_loop (&mut self) { + if let Some(clip) = self.clip() { + clip.write().unwrap().toggle_loop() + } + } +} diff --git a/crates/device/src/arranger/arranger_device.rs b/crates/device/src/arranger/arranger_device.rs new file mode 100644 index 00000000..60894ab5 --- /dev/null +++ b/crates/device/src/arranger/arranger_device.rs @@ -0,0 +1,27 @@ +use crate::*; + +impl Arrangement { + fn device_add_lv2 (&mut self) -> Usually<()> { + todo!(); + Ok(()) + } + fn device_add_sampler (&mut self) -> Usually<()> { + let name = self.jack.with_client(|c|c.name().to_string()); + let midi = self.track().expect("no active track").sequencer.midi_outs[0].name(); + let sampler = if let Ok(sampler) = Sampler::new( + &self.jack, + &format!("{}/Sampler", &self.track().expect("no active track").name), + &[PortConnect::exact(format!("{name}:{midi}"))], + &[&[], &[]], + &[&[], &[]] + ) { + self.dialog = None; + Device::Sampler(sampler) + } else { + self.dialog = Some(Dialog::Message(Message::FailedToAddDevice)); + return Err("failed to add device".into()) + }; + self.track_mut().expect("no active track").devices.push(sampler); + Ok(()) + } +} diff --git a/crates/device/src/arranger/arranger_edit.rs b/crates/device/src/arranger/arranger_edit.rs new file mode 100644 index 00000000..715623f1 --- /dev/null +++ b/crates/device/src/arranger/arranger_edit.rs @@ -0,0 +1,62 @@ +use crate::*; + +#[cfg(all(feature = "pool", feature = "editor"))] +pub trait AutoCreate: + Has> + + Has> + + Has> + + Has> + + Has> + + Send + Sync +{ + // Create new clip in pool when entering empty cell + fn clip_auto_create (&mut self) -> Option>> { + if let Some(pool) = Has::::get(self) + && let Some(Selection::TrackClip { track, scene }) = self.get() + && let Some(scene) = Has::>::get_mut(self).get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && slot.is_none() + && let Some(track) = Has::>::get_mut(self).get_mut(track) + { + let (index, mut clip) = pool.add_new_clip(); + // autocolor: new clip colors from scene and track color + let color = track.color.base.mix(scene.color.base, 0.5); + clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); + if let Some(ref mut editor) = Has::>::get_mut(self) { + editor.set_clip(Some(&clip)); + } + *slot = Some(clip.clone()); + Some(clip) + } else { + None + } + } +} + +#[cfg(all(feature = "pool", feature = "editor"))] +pub trait AutoRemove: + Has> + + Has> + + Has> + + Has> + + Has> + + Send + Sync +{ + // Remove clip from arrangement when exiting empty clip editor + fn clip_auto_remove (&mut self) { + if let Some(ref mut pool) = Has::::get(self) + && let Some(Selection::TrackClip { track, scene }) = self.get() + && let Some(scene) = Has::>::get_mut(self).get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && let Some(clip) = slot.as_mut() + { + let mut swapped = None; + if clip.read().unwrap().count_midi_messages() == 0 { + std::mem::swap(&mut swapped, slot); + } + if let Some(clip) = swapped { + pool.delete_clip(&clip.read().unwrap()); + } + } + } +} diff --git a/crates/device/src/arranger/arranger_model.rs b/crates/device/src/arranger/arranger_model.rs new file mode 100644 index 00000000..0be3e78b --- /dev/null +++ b/crates/device/src/arranger/arranger_model.rs @@ -0,0 +1,74 @@ +use crate::*; + +#[derive(Default, Debug)] +pub struct Arrangement { + /// Project name. + pub name: Arc, + /// Base color. + pub color: ItemTheme, + /// List of global midi inputs + pub midi_ins: Vec, + /// List of global midi outputs + pub midi_outs: Vec, + /// List of global audio inputs + pub audio_ins: Vec, + /// List of global audio outputs + pub audio_outs: Vec, + /// Buffer for writing a midi event + pub note_buf: Vec, + /// Buffer for writing a chunk of midi events + pub midi_buf: Vec>>, + /// Last track number (to avoid duplicate port names) + pub track_last: usize, + /// List of tracks + pub tracks: Vec, + /// Scroll offset of tracks + pub track_scroll: usize, + /// List of scenes + pub scenes: Vec, + /// Scroll offset of scenes + pub scene_scroll: usize, + /// Selected UI element + pub selected: Option, + /// Contains a render of the project arrangement, redrawn on update. + /// TODO rename to "render_cache" or smth + pub arranger: Arc>, + /// Display size + pub size: Measure, +} + +has!(Option: |self: Arrangement|self.selected); +has!(Vec: |self: Arrangement|self.midi_ins); +has!(Vec: |self: Arrangement|self.midi_outs); +has!(Vec: |self: Arrangement|self.scenes); +has!(Vec: |self: Arrangement|self.tracks); + +impl Has> for Arrangement { + fn get (&self) -> &Option { + Has::::get(self) + .and_then(|selection|selection.track()) + .and_then(|index|Has::>::get(self).get(index)) + .flatten() + } + fn get_mut (&mut self) -> &mut Option { + Has::::get(self) + .and_then(|selection|selection.track()) + .and_then(|index|Has::>::get_mut(self).get_mut(index)) + .flatten() + } +} + +impl Has> for Arrangement { + fn get (&self) -> &Option { + Has::::get(self) + .and_then(|selection|selection.track()) + .and_then(|index|Has::>::get(self).get(index)) + .flatten() + } + fn get_mut (&mut self) -> &mut Option { + Has::::get(self) + .and_then(|selection|selection.track()) + .and_then(|index|Has::>::get_mut(self).get_mut(index)) + .flatten() + } +} diff --git a/crates/device/src/arranger/arranger_port.rs b/crates/device/src/arranger/arranger_port.rs new file mode 100644 index 00000000..9b09ba84 --- /dev/null +++ b/crates/device/src/arranger/arranger_port.rs @@ -0,0 +1,134 @@ +use crate::*; + +impl<'a> ArrangerView<'a> { + + pub(crate) fn input_routes (&'a self) -> impl Content + 'a { + Tryptich::top(self.inputs_height) + .left(self.width_side, + io_ports(Tui::g(224), Tui::g(32), ||self.arrangement.midi_ins_with_sizes())) + .middle(self.width_mid, + per_track_top(self.width_mid, ||self.tracks_with_sizes_scrolled(), + move|_, &Track { color, .. }|io_conns( + color.dark.rgb, + color.darker.rgb, + ||self.arrangement.midi_ins_with_sizes() + ))) + } + + pub(crate) fn input_ports (&'a self) -> impl Content + 'a { + Tryptich::top(1) + .left(self.width_side, + button_3("i", "midi ins", format!("{}", self.inputs_count), self.is_editing)) + .right(self.width_side, + button_2("I", "add midi in", self.is_editing)) + .middle(self.width_mid, + per_track_top( + self.width_mid, + ||self.tracks_with_sizes_scrolled(), + move|t, track|{ + let rec = track.sequencer.recording; + let mon = track.sequencer.monitoring; + let rec = if rec { White } else { track.color.darkest.rgb }; + let mon = if mon { White } else { track.color.darkest.rgb }; + let bg = if self.track_selected == Some(t) { + track.color.light.rgb + } else { + track.color.base.rgb + }; + //let bg2 = if t > 0 { track.color.base.rgb } else { Reset }; + wrap(bg, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e( + Tui::fg_bg(rec, bg, "Rec "), + Tui::fg_bg(mon, bg, "Mon "), + )))) + })) + } + + pub(crate) fn input_intos (&'a self) -> impl Content + 'a { + Tryptich::top(2) + .left(self.width_side, + Bsp::s(Align::e("Input:"), Align::e("Into clip:"))) + .middle(self.width_mid, + per_track_top( + self.width_mid, + ||self.tracks_with_sizes_scrolled(), + |_, _|Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ "))))) + } + + pub(crate) fn output_nexts (&self) -> impl Content + '_ { + Tryptich::top(2) + .left(self.width_side, Align::ne("From clip:")) + .middle(self.width_mid, per_track_top( + self.width_mid, + ||self.tracks_with_sizes_scrolled(), + |_, _|Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default()))))) + } + + pub(crate) fn output_froms (&'a self) -> impl Content + 'a { + let label = Align::ne("Next clip:"); + Tryptich::top(2).left(self.width_side, label).middle(self.width_mid, per_track_top( + self.width_mid, ||self.tracks_with_sizes_scrolled(), |t, track|{ + let queued = track.sequencer.next_clip.is_some(); + let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ ")); + let queued_clip = Thunk::new(||{ + Tui::bg(Reset, if let Some((_, clip)) = track.sequencer.next_clip.as_ref() { + if let Some(clip) = clip { + clip.read().unwrap().name.clone() + } else { + "Stop".into() + } + } else { + "".into() + }) + }); + Either(queued, queued_clip, queued_blank) + })) + } + + pub(crate) fn output_ports (&'a self) -> impl Content + 'a { + Tryptich::top(1) + .left(self.width_side, self.output_count()) + .right(self.width_side, self.output_add()) + .middle(self.width_mid, self.output_map()) + } + + pub(crate) fn output_count (&'a self) -> impl Content + 'a { + button_3("o", "midi outs", format!("{}", self.outputs_count), self.is_editing) + } + + pub(crate) fn output_add (&'a self) -> impl Content + 'a { + button_2("O", "add midi out", self.is_editing) + } + + pub(crate) fn output_map (&'a self) -> impl Content + 'a { + per_track_top(self.width_mid, ||self.tracks_with_sizes_scrolled(), move|i, t|{ + let mute = false; + let solo = false; + let mute = if mute { White } else { t.color.darkest.rgb }; + let solo = if solo { White } else { t.color.darkest.rgb }; + let bg_1 = if self.track_selected == Some(i) { + t.color.light.rgb + } else { + t.color.base.rgb + }; + let bg_2 = if i > 0 { t.color.base.rgb } else { Reset }; + let mute = Tui::fg_bg(mute, bg_1, "Play "); + let solo = Tui::fg_bg(solo, bg_1, "Solo "); + wrap(bg_1, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(mute, solo)))) + }) + } + + pub(crate) fn output_conns (&'a self) -> impl Content + 'a { + Tryptich::top(self.outputs_height) + .left(self.width_side, io_ports( + Tui::g(224), Tui::g(32), ||self.arrangement.midi_outs_with_sizes())) + .middle(self.width_mid, per_track_top( + self.width_mid, + ||self.tracks_with_sizes_scrolled(), + |_, t|io_conns( + t.color.dark.rgb, + t.color.darker.rgb, + ||self.arrangement.midi_outs_with_sizes() + ))) + } + +} diff --git a/crates/device/src/arranger/arranger_scene.rs b/crates/device/src/arranger/arranger_scene.rs new file mode 100644 index 00000000..6fb3401c --- /dev/null +++ b/crates/device/src/arranger/arranger_scene.rs @@ -0,0 +1,261 @@ +use crate::*; + +#[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>>>, +} + +impl Scene { + /// Returns the pulse length of the longest clip in the scene + pub fn pulses (&self) -> usize { + self.clips.iter().fold(0, |a, p|{ + a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) + }) + } + /// Returns true if all clips in the scene are + /// currently playing on the given collection of tracks. + pub fn is_playing (&self, tracks: &[Track]) -> bool { + self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() + .all(|(track_index, clip)|match clip { + Some(c) => tracks + .get(track_index) + .map(|track|{ + if let Some((_, Some(clip))) = track.sequencer().play_clip() { + *clip.read().unwrap() == *c.read().unwrap() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) + } + pub fn clip (&self, index: usize) -> Option<&Arc>> { + match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } + } +} + +impl> + Send + Sync> HasScenes for T {} + +pub trait HasScenes: Has> + Send + Sync { + fn scenes (&self) -> &Vec { + Has::>::get(self) + } + fn scenes_mut (&self) -> &mut Vec { + Has::>::get_mut(self) + } + fn scenes_with_sizes ( + &self, + editing: bool, + height: usize, + larger: usize, + selected_track: Option, + selected_scene: Option, + ) -> impl ScenesSizes<'_> { + let mut y = 0; + self.scenes().iter().enumerate().map(move|(s, scene)|{ + let active = editing && selected_track.is_some() && selected_scene == Some(s); + let height = if active { larger } else { height }; + let data = (s, scene, y, y + height); + y += height; + data + }) + } + /// Generate the default name for a new scene + fn scene_default_name (&self) -> Arc { + format!("Sc{:3>}", self.scenes().len() + 1).into() + } + fn scene_longest_name (&self) -> usize { + self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) + } +} + +impl> + Send + Sync> HasScene for T {} + +pub trait HasScene: Has> + Send + Sync { + fn scene (&self) -> Option<&Scene> { + Has::>::get(self) + } + fn scene_mut (&mut self) -> Option<&mut Scene> { + Has::>::get_mut(self) + } +} + +impl<'a> ArrangerView<'a> { + /// Default scene height. + pub(crate) const H_SCENE: usize = 2; + /// Default editor height. + pub(crate) const H_EDITOR: usize = 15; + + /// Render scenes with clips + pub(crate) fn scenes (&'a self, editor: Option) -> impl Content + 'a { + + /// A scene with size and color. + type SceneWithColor<'a> = (usize, &'a Scene, usize, usize, Option); + + let Self { + arrangement, + width, width_side, width_mid, + scenes_height, scene_last, scene_selected, + track_selected, is_editing, .. + } = self; + + let selection = Has::>::get(*arrangement); + let (selected_track, selected_scene) = match selection { + Some(Selection::Track(t)) => (Some(*t), None), + Some(Selection::Scene(s)) => (None, Some(*s)), + Some(Selection::TrackClip { track, scene }) => (Some(*track), Some(*scene)), + _ => (None, None) + }; + + let scenes_with_scene_colors = ||arrangement.scenes_with_sizes( + *is_editing, + Self::H_SCENE, + Self::H_EDITOR, + selected_track, + selected_scene, + ).map_while(|(s, scene, y1, y2)|if y2 as u16 > *scenes_height { + None + } else { + Some((s, scene, y1, y2, if s == 0 { + None + } else { + Some(arrangement.scenes()[s-1].color) + })) + }); + + let scene_header = |(s, scene, y1, y2, previous): SceneWithColor, _|{ + let height = (1 + y2 - y1) as u16; + let name = Some(scene.name.clone()); + let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⯈ ", name)))); + let same_track = true; + let selected = same_track && *scene_selected == Some(s); + let neighbor = same_track && s > 0 && *scene_selected == Some(s - 1); + let is_last = *scene_last == s; + let theme = scene.color; + let fg = theme.lightest.rgb; + let bg = if selected { theme.light } else { theme.base }.rgb; + let hi = if let Some(previous) = previous { + if neighbor { + previous.light.rgb + } else { + previous.base.rgb + } + } else { + Reset + }; + let lo = if is_last { + Reset + } else if selected { + theme.light.rgb + } else { + theme.base.rgb + }; + Fill::x(map_south(y1 as u16, height, Fixed::y(height, Phat { + width: 0, height: 0, content, colors: [fg, bg, hi, lo] + }))) + }; + + let scenes_with_track_colors = |track: usize| arrangement.scenes_with_sizes( + self.is_editing, + Self::H_SCENE, + Self::H_EDITOR, + selected_track, + selected_scene, + ).map_while(|(s, scene, y1, y2)|if y2 as u16 > self.scenes_height { + None + } else { + Some((s, scene, y1, y2, if s == 0 { + None + } else { + Some(self.arrangement.scenes[s-1].clips[track].as_ref() + .map(|c|c.read().unwrap().color) + .unwrap_or(ItemTheme::G[32])) + })) + }); + + let scene_track_clips = |track_index, track|Map::new( + ||scenes_with_track_colors(track_index), + |(s, scene, y1, y2, previous): SceneWithColor<'a>, _|{ + let (name, theme) = if let Some(clip) = &scene.clips[track_index] { + let clip = clip.read().unwrap(); + (Some(clip.name.clone()), clip.color) + } else { + (None, ItemTheme::G[32]) + }; + let height = (1 + y2 - y1) as u16; + let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⏹ ", name)))); + let same_track = *track_selected == Some(track_index); + let selected = same_track && *scene_selected == Some(s); + let neighbor = same_track && s > 0 && *scene_selected == Some(s - 1); + let is_last = *scene_last == s; + let fg = theme.lightest.rgb; + let bg = if selected { theme.light } else { theme.base }.rgb; + let hi = if let Some(previous) = previous { + if neighbor { + previous.light.rgb + } else { + previous.base.rgb + } + } else { + Reset + }; + let lo = if is_last { + Reset + } else if selected { + theme.light.rgb + } else { + theme.base.rgb + }; + map_south(y1 as u16, height, Bsp::b(Fixed::y(height, Phat { + width: 0, height: 0, content, colors: [fg, bg, hi, lo] + }), When( + *is_editing && same_track && *scene_selected == Some(s), + editor + ))) + }); + + Tryptich::center(*scenes_height) + .left(*width_side, Map::new( + ||scenes_with_scene_colors(), + scene_header)) + .middle(*width_mid, per_track( + *width_mid, + ||self.tracks_with_sizes_scrolled(), + scene_track_clips)) + } + +} + +impl Arrangement { + /// Add multiple scenes + pub fn scenes_add (&mut self, n: usize) -> Usually<()> { + let scene_color_1 = ItemColor::random(); + let scene_color_2 = ItemColor::random(); + for i in 0..n { + let _ = self.scene_add(None, Some( + scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() + ))?; + } + Ok(()) + } + + /// Add a scene + pub fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<(usize, &mut Scene)> + { + let scene = Scene { + name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), + clips: vec![None;self.tracks().len()], + color: color.unwrap_or_else(ItemTheme::random), + }; + self.scenes_mut().push(scene); + let index = self.scenes().len() - 1; + Ok((index, &mut self.scenes_mut()[index])) + } +} diff --git a/crates/app/src/model/selection.rs b/crates/device/src/arranger/arranger_select.rs similarity index 67% rename from crates/app/src/model/selection.rs rename to crates/device/src/arranger/arranger_select.rs index d4560a94..6105c200 100644 --- a/crates/app/src/model/selection.rs +++ b/crates/device/src/arranger/arranger_select.rs @@ -1,10 +1,5 @@ use crate::*; -pub trait HasSelection { - fn selected (&self) -> &Selection; - fn selected_mut (&mut self) -> &mut Selection; -} - /// Represents the current user selection in the arranger #[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { @@ -151,7 +146,70 @@ impl Selection { } } -impl HasSelection for App { - fn selected (&self) -> &Selection { &self.selected } - fn selected_mut (&mut self) -> &mut Selection { &mut self.selected } +impl Arrangement { + /// Set the selection + pub(crate) fn select (&mut self, s: Selection) { + self.selected = s; + // autoedit: load focused clip in editor. + if let Some(ref mut editor) = self.editor { + editor.set_clip(match self.selected { + Selection::TrackClip { track, scene } + if let Some(Some(Some(clip))) = self + .scenes.get(scene) + .map(|s|s.clips.get(track)) => Some(clip), + _ => None + }); + } + } + /// Launch a clip or scene + pub(crate) fn launch (&mut self) { + use Selection::*; + match self.selected { + Track(t) => { + self.tracks[t].sequencer.enqueue_next(None) + }, + TrackClip { track, scene } => { + self.tracks[track].sequencer.enqueue_next(self.scenes[scene].clips[track].as_ref()) + }, + Scene(s) => { + for t in 0..self.tracks.len() { + self.tracks[t].sequencer.enqueue_next(self.scenes[s].clips[t].as_ref()) + } + }, + _ => {} + }; + } + /// Set the color of the selected entity + pub fn set_color (&mut self, palette: Option) -> Option { + use Selection::*; + let palette = palette.unwrap_or_else(||ItemTheme::random()); + Some(match self.selected { + Mix => { + let old = self.color; + self.color = palette; + old + }, + Scene(s) => { + let old = self.scenes[s].color; + self.scenes[s].color = palette; + old + } + Track(t) => { + let old = self.tracks[t].color; + self.tracks[t].color = palette; + old + } + TrackClip { track, scene } => { + if let Some(ref clip) = self.scenes[scene].clips[track] { + let mut clip = clip.write().unwrap(); + let old = clip.color; + clip.color = palette; + old + } else { + return None + } + }, + _ => todo!() + }) + } } diff --git a/crates/device/src/arranger/arranger_track.rs b/crates/device/src/arranger/arranger_track.rs new file mode 100644 index 00000000..fc028ee5 --- /dev/null +++ b/crates/device/src/arranger/arranger_track.rs @@ -0,0 +1,290 @@ +use crate::*; + +#[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, +} +has_clock!(|self: Track|self.sequencer.clock); +has_sequencer!(|self: Track|self.sequencer); + +impl Track { + /// Create a new track with only the default [Sequencer]. + pub fn new ( + name: &impl AsRef, + color: Option, + jack: &Jack, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[PortConnect], + midi_to: &[PortConnect], + ) -> Usually { + Ok(Self { + name: name.as_ref().into(), + color: color.unwrap_or_default(), + sequencer: Sequencer::new( + format!("{}/sequencer", name.as_ref()), + jack, + clock, + clip, + midi_from, + midi_to + )?, + ..Default::default() + }) + } + /// Create a new track connecting the [Sequencer] to a [Sampler]. + pub fn new_with_sampler ( + name: &impl AsRef, + color: Option, + jack: &Jack, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[PortConnect], + midi_to: &[PortConnect], + audio_from: &[&[PortConnect];2], + audio_to: &[&[PortConnect];2], + ) -> Usually { + let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; + track.devices.push(Device::Sampler(Sampler::new( + jack, + &format!("{}/sampler", name.as_ref()), + &[PortConnect::exact(format!("{}:{}", + jack.with_client(|c|c.name().to_string()), + track.sequencer.midi_outs[0].name() + ))], + audio_from, + audio_to + )?)); + Ok(track) + } + #[cfg(feature = "sampler")] + pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { + for device in self.devices.iter() { + match device { + Device::Sampler(s) => if nth == 0 { + return Some(s); + } else { + nth -= 1; + }, + _ => {} + } + } + None + } + #[cfg(feature = "sampler")] + pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { + for device in self.devices.iter_mut() { + match device { + Device::Sampler(s) => if nth == 0 { + return Some(s); + } else { + nth -= 1; + }, + _ => {} + } + } + None + } +} + +impl> + Send + Sync> HasTracks for T {} + +pub trait HasTracks: Send + Sync { + fn tracks (&self) -> &Vec { + Has::>::get(self) + } + fn tracks_mut (&mut self) -> &mut Vec { + Has::>::get_mut(self) + } + /// Run audio callbacks for every track and every device + fn tracks_render (&mut self, client: &Client, scope: &ProcessScope) -> Control { + for track in self.tracks.iter_mut() { + if Control::Quit == PlayerAudio( + track.sequencer_mut(), &mut self.note_buf, &mut self.midi_buf + ).process(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); + } + } else { + for track in self.tracks_mut().iter_mut() { + track.sequencer.enqueue_next(None); + } + } + } + /// Iterate over tracks with their corresponding sizes. + fn tracks_with_sizes ( + &self, + selection: &Option, + editor_width: Option + ) -> impl TracksSizes<'_> { + let mut x = 0; + let active_track = if let Some(width) = editor_width && let Some(selection) = selection { + selection.track() + } else { + None + }; + self.tracks().iter().enumerate().map(move |(index, track)|{ + let width = active_track + .and_then(|_|editor_width) + .unwrap_or(track.width.max(8)); + let data = (index, track, x, x + width); + x += width + Self::TRACK_SPACING; + data + }) + } + /// Spacing between tracks. + const TRACK_SPACING: usize = 0; +} + +impl> + Send + Sync> HasTrack for T {} + +pub trait HasTrack: Has> + Send + Sync { + fn scene (&self) -> Option<&Track> { + Has::>::get(self) + } + fn scene_mut (&mut self) -> Option<&mut Track> { + Has::>::get_mut(self) + } +} + +impl<'a> ArrangerView<'a> { + pub(crate) fn tracks_with_sizes_scrolled (&'a self) + -> impl TracksSizes<'a> + { + self.arrangement + .tracks_with_sizes(&self.arrangement.selected, self.is_editing.then_some(20/*FIXME*/)) + .map_while(move|(t, track, x1, x2)|{ + (self.width_mid > x2 as u16).then_some((t, track, x1, x2)) + }) + } +} + +pub(crate) fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( + width: u16, + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Content + 'a { + Align::x(Tui::bg(Reset, Map::new(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + let width = (x2 - x1) as u16; + map_east(x1 as u16, width, Fixed::x(width, Tui::fg_bg( + track.color.lightest.rgb, + track.color.base.rgb, + callback(index, track))))}))) +} + +pub(crate) fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( + width: u16, + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Content + 'a { + per_track_top( + width, + tracks, + move|index, track|Fill::y(Align::y(callback(index, track))) + ) +} + +impl Arrangement { + /// Add multiple tracks + pub fn tracks_add ( + &mut self, + count: usize, + width: Option, + mins: &[PortConnect], + mouts: &[PortConnect], + ) -> Usually<()> { + let jack = self.jack().clone(); + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..count { + let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); + let mut track = self.track_add(None, Some(color), mins, mouts)?.1; + if let Some(width) = width { + track.width = width; + } + } + Ok(()) + } + + /// Add a track + pub fn track_add ( + &mut self, + name: Option<&str>, + color: Option, + mins: &[PortConnect], + mouts: &[PortConnect], + ) -> Usually<(usize, &mut Track)> { + self.track_last += 1; + let name: Arc = name.map_or_else( + ||format!("Track{:02}", self.track_last).into(), + |x|x.to_string().into() + ); + let mut track = Track { + width: (name.len() + 2).max(12), + color: color.unwrap_or_else(ItemTheme::random), + sequencer: Sequencer::new( + &format!("{name}"), + self.jack(), + Some(self.clock()), + None, + mins, + mouts + )?, + name, + ..Default::default() + }; + self.tracks_mut().push(track); + let len = self.tracks().len(); + let index = len - 1; + for scene in self.scenes_mut().iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); + } + } + Ok((index, &mut self.tracks_mut()[index])) + } +} + +#[cfg(feature = "sampler")] +impl Arrangement { + /// Get the first sampler of the active track + pub fn sampler (&self) -> Option<&Sampler> { + self.track().map(|t|t.sampler(0)).flatten() + } + /// Get the first sampler of the active track + pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { + self.track_mut().map(|t|t.sampler_mut(0)).flatten() + } +} diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs new file mode 100644 index 00000000..f94b89aa --- /dev/null +++ b/crates/device/src/arranger/arranger_view.rs @@ -0,0 +1,217 @@ +use crate::*; + +impl Arrangement { + /// Width of display + pub(crate) fn w (&self) -> u16 { + self.size.w() as u16 + } + /// Width allocated for sidebar. + pub(crate) fn w_sidebar (&self) -> u16 { + self.w() / if self.is_editing() { 16 } else { 8 } as u16 + } + /// Width taken by all tracks. + pub(crate) fn w_tracks (&self) -> u16 { + self.tracks_with_sizes().last().map(|(_, _, _, x)|x as u16).unwrap_or(0) + } + /// Width available to display tracks. + pub(crate) fn w_tracks_area (&self) -> u16 { + self.w().saturating_sub(2 * self.w_sidebar()) + } + /// Height of display + pub(crate) fn h (&self) -> u16 { + self.size.h() as u16 + } + /// Height available to display track headers. + pub(crate) fn h_tracks_area (&self) -> u16 { + 5 // FIXME + //self.h().saturating_sub(self.h_inputs() + self.h_outputs()) + } + /// Height available to display tracks. + pub(crate) fn h_scenes_area (&self) -> u16 { + //15 + self.h().saturating_sub( + self.h_inputs() + + self.h_outputs() + + self.h_devices() + + 13 // FIXME + ) + } + /// Height taken by all scenes. + pub(crate) fn h_scenes (&self) -> u16 { + let (selected_track, selected_scene) = match Has::>::get(self) { + Some(Selection::Track(t)) => (Some(*t), None), + Some(Selection::Scene(s)) => (None, Some(*s)), + Some(Selection::TrackClip { track, scene }) => (Some(*track), Some(*scene)), + _ => (None, None) + }; + self.scenes_with_sizes( + self.is_editing, + ArrangerView::H_SCENE, + ArrangerView::H_EDITOR, + selected_track, + selected_scene + ) + .last() + .map(|(_, _, _, y)|y as u16).unwrap_or(0) + } + /// Height taken by all inputs. + pub(crate) fn h_inputs (&self) -> u16 { + self.inputs_with_sizes() + .last() + .map(|(_, _, _, _, y)|y as u16).unwrap_or(0) + } + /// Height taken by all outputs. + pub(crate) fn h_outputs (&self) -> u16 { + self.outputs_with_sizes() + .last() + .map(|(_, _, _, _, y)|y as u16).unwrap_or(0) + } + /// Height taken by visible device slots. + pub(crate) fn h_devices (&self) -> u16 { + 2 + //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) + } +} + +pub(crate) struct ArrangerView<'a> { + pub arrangement: &'a Arrangement, + + pub is_editing: bool, + + pub width: u16, + pub width_mid: u16, + pub width_side: u16, + + pub inputs_count: usize, + pub inputs_height: u16, + + pub outputs_count: usize, + pub outputs_height: u16, + + pub scene_last: usize, + pub scene_count: usize, + pub scene_scroll: Fill>, + pub scene_selected: Option, + pub scenes_height: u16, + + pub track_scroll: Fill>, + pub track_count: usize, + pub track_selected: Option, + pub tracks_height: u16, + + pub show_debug_info: bool, +} + +impl<'a> ArrangerView<'a> { + pub fn new ( + arrangement: &'a Arrangement, + editor: Option<&'a MidiEditor> + ) -> Self { + let selected = arrangement.selected; + let h_tracks_area = arrangement.h_tracks_area(); + let h_scenes_area = arrangement.h_scenes_area(); + let h_scenes = arrangement.h_scenes(); + Self { + arrangement, + is_editing: editor.is_some(), + + width: arrangement.w(), + width_mid: arrangement.w_tracks_area(), + width_side: arrangement.w_sidebar(), + + inputs_height: arrangement.h_inputs(), + inputs_count: arrangement.midi_ins.len(), + + outputs_height: arrangement.h_outputs(), + outputs_count: arrangement.midi_outs.len(), + + scenes_height: h_scenes_area, + scene_selected: selected.map(|s|s.scene()).flatten(), + scene_count: arrangement.scenes.len(), + scene_last: arrangement.scenes.len().saturating_sub(1), + scene_scroll: Fill::y(Fixed::x(1, ScrollbarV { + offset: arrangement.scene_scroll, + length: h_scenes_area as usize, + total: h_scenes as usize, + })), + + tracks_height: h_tracks_area, + track_count: arrangement.tracks.len(), + track_selected: selected.map(|s|s.track()).flatten(), + track_scroll: Fill::x(Fixed::y(1, ScrollbarH { + offset: arrangement.track_scroll, + length: h_tracks_area as usize, + total: h_scenes as usize, + })), + + show_debug_info: false + } + } +} + +impl<'a> Content for ArrangerView<'a> { + fn content (&self) -> impl Render { + let ins = |x|Bsp::n(self.inputs(), x); + let tracks = |x|Bsp::s(self.tracks(), x); + let devices = |x|Bsp::s(self.devices(), x); + let outs = |x|Bsp::s(self.outputs(), x); + let bg = |x|Tui::bg(Reset, x); + //let track_scroll = |x|Bsp::s(&self.track_scroll, x); + //let scene_scroll = |x|Bsp::e(&self.scene_scroll, x); + outs(tracks(devices(ins(bg(self.scenes(None)))))) + } +} + +impl<'a> ArrangerView<'a> { + /// Render input matrix. + pub(crate) fn inputs (&'a self) -> impl Content + 'a { + Tui::bg(Reset, Bsp::s( + self.input_intos(), + Bsp::s(self.input_routes(), self.input_ports()), + )) + } + + /// Render output matrix. + pub(crate) fn outputs (&'a self) -> impl Content + 'a { + Tui::bg(Reset, Align::n(Bsp::s( + Bsp::s(self.output_ports(), self.output_conns()), + Bsp::s(self.output_nexts(), self.output_froms()), + ))) + } + + /// Render track headers + pub(crate) fn tracks (&'a self) -> impl Content + 'a { + let Self { width_side, width_mid, track_count, track_selected, is_editing, .. } = self; + Tryptich::center(3) + .left(*width_side, button_3("t", "track", format!("{}", *track_count), *is_editing)) + .right(*width_side, button_2("T", "add track", *is_editing)) + .middle(*width_mid, per_track(*width_mid, ||self.tracks_with_sizes_scrolled(), + |index, track|wrap( + if *track_selected == Some(index) { + track.color.light + } else { + track.color.base + }.rgb, + track.color.lightest.rgb, + Tui::bold(true, Fill::xy(Align::nw(&track.name))) + ))) + } + + /// Render device switches. + pub(crate) fn devices (&'a self) -> impl Content + 'a { + let Self { width_side, width_mid, track_count, track_selected, is_editing, .. } = self; + Tryptich::top(1) + .left(*width_side, button_3("d", "devices", format!("{}", 0), *is_editing)) + .right(*width_side, button_2("D", "add device", *is_editing)) + .middle(*width_mid, per_track_top(*width_mid, ||self.tracks_with_sizes_scrolled(), + move|index, track|{ + let bg = if *track_selected == Some(index) { + track.color.light + } else { + track.color.base + }; + let fg = Tui::g(224); + track.devices.get(0).map(|device|wrap(bg.rgb, fg, device.name())) + })) + } +} diff --git a/crates/device/src/browser/browser_api.rs b/crates/device/src/browser/browser_api.rs index 349a895b..1f80e1f8 100644 --- a/crates/device/src/browser/browser_api.rs +++ b/crates/device/src/browser/browser_api.rs @@ -2,19 +2,31 @@ use crate::*; #[tengri_proc::expose] impl Browser { + fn _todo_stub_path_buf (&self) -> PathBuf { + todo!() + } + fn _todo_stub_usize (&self) -> usize { + todo!() + } + fn _todo_stub_arc_str (&self) -> Arc { + todo!() + } } #[tengri_proc::command(Browser)] impl BrowserCommand { - //fn set_address (browser: &mut Browser, address: PathBuf) -> Perhaps { - //Ok(None) - //} - //fn set_search (browser: &mut Browser, filter: Arc) -> Perhaps { - //Ok(None) - //} - //fn set_cursor (browser: &mut Browser, cursor: usize) -> Perhaps { - //Ok(None) - //} + fn set_visible (browser: &mut Browser) -> Perhaps { + Ok(None) + } + fn set_path (browser: &mut Browser, address: PathBuf) -> Perhaps { + Ok(None) + } + fn set_search (browser: &mut Browser, filter: Arc) -> Perhaps { + Ok(None) + } + fn set_cursor (browser: &mut Browser, cursor: usize) -> Perhaps { + Ok(None) + } } // Commands supported by [Browser] diff --git a/crates/device/src/browser/browser_model.rs b/crates/device/src/browser/browser_model.rs index 053ecbce..c06caff6 100644 --- a/crates/device/src/browser/browser_model.rs +++ b/crates/device/src/browser/browser_model.rs @@ -1,15 +1,17 @@ use crate::*; +use std::path::PathBuf; +use std::ffi::OsString; /// Browses for phrase to import/export #[derive(Debug, Clone)] pub struct Browser { - 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 cwd: PathBuf, + pub dirs: Vec<(OsString, String)>, + pub files: Vec<(OsString, String)>, + pub filter: String, + pub index: usize, + pub scroll: usize, + pub size: Measure, } impl Browser { diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 7bc424a2..7c2a0c01 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -1,4 +1,6 @@ #![feature(let_chains)] +#![feature(trait_alias)] +#![feature(if_let_guard)] pub(crate) use std::cmp::Ord; pub(crate) use std::fmt::{Debug, Formatter}; @@ -10,7 +12,7 @@ pub(crate) use std::path::PathBuf; pub(crate) use std::error::Error; pub(crate) use std::ffi::OsString; -pub(crate) use ::tengri::{Usually, Perhaps, dsl::*, input::*, output::*, tui::{*, ratatui::prelude::*}}; +pub(crate) use ::tengri::{from, Usually, Perhaps, dsl::*, input::*, output::*, tui::{*, ratatui::prelude::*}}; pub(crate) use ::tek_engine::*; pub(crate) use ::tek_engine::midi::{u7, LiveEvent, MidiMessage}; pub(crate) use ::tek_engine::jack::{Control, ProcessScope, MidiWriter, RawMidi}; @@ -20,6 +22,9 @@ pub(crate) use Color::*; mod device; pub use self::device::*; +#[cfg(feature = "arranger")] mod arranger; +#[cfg(feature = "arranger")] pub use self::arranger::*; + #[cfg(feature = "browser")] mod browser; #[cfg(feature = "browser")] pub use self::browser::*; diff --git a/crates/device/src/sequencer/seq_audio.rs b/crates/device/src/sequencer/seq_audio.rs index 0b78b210..c96b7cdc 100644 --- a/crates/device/src/sequencer/seq_audio.rs +++ b/crates/device/src/sequencer/seq_audio.rs @@ -22,11 +22,11 @@ impl Audio for PlayerAudio<'_, T> { if model.play(scope, note_buf, midi_buf) { model.switchover(scope, note_buf, midi_buf); } - if model.has_midi_ins() { + if !model.midi_ins().is_empty() { if model.recording() || model.monitoring() { // Record and/or monitor input model.record(scope, midi_buf) - } else if model.has_midi_outs() && model.monitoring() { + } else if model.midi_outs().is_empty() && model.monitoring() { // Monitor input to output model.monitor(scope, midi_buf) } diff --git a/crates/device/src/sequencer/seq_model.rs b/crates/device/src/sequencer/seq_model.rs index 298364ac..2282edb1 100644 --- a/crates/device/src/sequencer/seq_model.rs +++ b/crates/device/src/sequencer/seq_model.rs @@ -103,17 +103,8 @@ impl std::fmt::Debug for Sequencer { } has_clock!(|self: Sequencer|self.clock); - -impl HasMidiIns for Sequencer { - fn midi_ins (&self) -> &Vec { &self.midi_ins } - fn midi_ins_mut (&mut self) -> &mut Vec { &mut self.midi_ins } -} - -impl HasMidiOuts for Sequencer { - fn midi_outs (&self) -> &Vec { &self.midi_outs } - fn midi_outs_mut (&mut self) -> &mut Vec { &mut self.midi_outs } - fn midi_note (&mut self) -> &mut Vec { &mut self.note_buf } -} +has!(Vec: |self:Sequencer| self.midi_ins); +has!(Vec: |self:Sequencer| self.midi_outs); impl MidiRecorder for Sequencer { fn recording (&self) -> bool { diff --git a/crates/engine/src/jack/jack_port.rs b/crates/engine/src/jack/jack_port.rs index 925d9fb8..85136cb9 100644 --- a/crates/engine/src/jack/jack_port.rs +++ b/crates/engine/src/jack/jack_port.rs @@ -1,94 +1,6 @@ use crate::*; use super::*; -macro_rules! impl_port { - ($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => { - #[derive(Debug)] pub struct $Name { - /// Handle to JACK client, for receiving reconnect events. - jack: Jack, - /// Port name - name: Arc, - /// Port handle. - port: Port<$Spec>, - /// List of ports to connect to. - conn: Vec - } - impl AsRef> for $Name { fn as_ref (&self) -> &Port<$Spec> { &self.port } } - impl $Name { - pub fn new ($jack: &Jack, name: impl AsRef, connect: &[PortConnect]) - -> Usually - { - let $name = name.as_ref(); - let jack = $jack.clone(); - let port = $port?; - let name = $name.into(); - let conn = connect.to_vec(); - let port = Self { jack, port, name, conn }; - port.connect_to_matching()?; - Ok(port) - } - pub fn name (&self) -> &Arc { &self.name } - pub fn port (&self) -> &Port<$Spec> { &self.port } - pub fn port_mut (&mut self) -> &mut Port<$Spec> { &mut self.port } - pub fn into_port (self) -> Port<$Spec> { self.port } - pub fn close (self) -> Usually<()> { - let Self { jack, port, .. } = self; - Ok(jack.with_client(|client|client.unregister_port(port))?) - } - } - impl HasJack for $Name { fn jack (&self) -> &Jack { &self.jack } } - impl JackPort for $Name { - type Port = $Spec; - type Pair = $Pair; - fn port (&self) -> &Port<$Spec> { &self.port } - } - impl JackPortConnect<&str> for $Name { - fn connect_to (&self, to: &str) -> Usually { - self.with_client(|c|if let Some(ref port) = c.port_by_name(to.as_ref()) { - self.connect_to(port) - } else { - Ok(Missing) - }) - } - } - impl JackPortConnect<&Port> for $Name { - fn connect_to (&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 - })) - } - } - impl JackPortConnect<&Port<$Pair>> for $Name { - fn connect_to (&self, port: &Port<$Pair>) -> 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 - })) - } - } - impl JackPortAutoconnect for $Name { - fn conn (&self) -> &[PortConnect] { - &self.conn - } - } - }; -} - -impl_port!(JackAudioIn: AudioIn -> AudioOut |j, n|j.register_port::(n)); - -impl_port!(JackAudioOut: AudioOut -> AudioIn |j, n|j.register_port::(n)); - -impl_port!(JackMidiIn: MidiIn -> MidiOut |j, n|j.register_port::(n)); - -impl_port!(JackMidiOut: MidiOut -> MidiIn |j, n|j.register_port::(n)); - pub trait JackPort: HasJack { type Port: PortSpec; type Pair: PortSpec; @@ -235,3 +147,91 @@ impl PortConnect { format!(" ({}) {} {}", status, scope, name).into() } } + +macro_rules! impl_port { + ($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => { + #[derive(Debug)] pub struct $Name { + /// Handle to JACK client, for receiving reconnect events. + jack: Jack, + /// Port name + name: Arc, + /// Port handle. + port: Port<$Spec>, + /// List of ports to connect to. + conn: Vec + } + impl AsRef> for $Name { fn as_ref (&self) -> &Port<$Spec> { &self.port } } + impl $Name { + pub fn new ($jack: &Jack, name: impl AsRef, connect: &[PortConnect]) + -> Usually + { + let $name = name.as_ref(); + let jack = $jack.clone(); + let port = $port?; + let name = $name.into(); + let conn = connect.to_vec(); + let port = Self { jack, port, name, conn }; + port.connect_to_matching()?; + Ok(port) + } + pub fn name (&self) -> &Arc { &self.name } + pub fn port (&self) -> &Port<$Spec> { &self.port } + pub fn port_mut (&mut self) -> &mut Port<$Spec> { &mut self.port } + pub fn into_port (self) -> Port<$Spec> { self.port } + pub fn close (self) -> Usually<()> { + let Self { jack, port, .. } = self; + Ok(jack.with_client(|client|client.unregister_port(port))?) + } + } + impl HasJack for $Name { fn jack (&self) -> &Jack { &self.jack } } + impl JackPort for $Name { + type Port = $Spec; + type Pair = $Pair; + fn port (&self) -> &Port<$Spec> { &self.port } + } + impl JackPortConnect<&str> for $Name { + fn connect_to (&self, to: &str) -> Usually { + self.with_client(|c|if let Some(ref port) = c.port_by_name(to.as_ref()) { + self.connect_to(port) + } else { + Ok(Missing) + }) + } + } + impl JackPortConnect<&Port> for $Name { + fn connect_to (&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 + })) + } + } + impl JackPortConnect<&Port<$Pair>> for $Name { + fn connect_to (&self, port: &Port<$Pair>) -> 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 + })) + } + } + impl JackPortAutoconnect for $Name { + fn conn (&self) -> &[PortConnect] { + &self.conn + } + } + }; +} + +impl_port!(JackAudioIn: AudioIn -> AudioOut |j, n|j.register_port::(n)); + +impl_port!(JackAudioOut: AudioOut -> AudioIn |j, n|j.register_port::(n)); + +impl_port!(JackMidiIn: MidiIn -> MidiOut |j, n|j.register_port::(n)); + +impl_port!(JackMidiOut: MidiOut -> MidiIn |j, n|j.register_port::(n)); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index db0bf96e..67482aa5 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -9,10 +9,29 @@ pub(crate) use std::sync::{Arc, atomic::{AtomicUsize, AtomicBool, Ordering::Rela pub(crate) use std::fmt::Debug; pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem}; -pub(crate) use ::tengri::{Usually, tui::*}; +pub(crate) use ::tengri::{from, Usually, tui::*}; pub use ::atomic_float; pub(crate) use atomic_float::*; +pub trait Has: Send + Sync { + fn get (&self) -> &T; + fn get_mut (&mut self) -> &mut T; +} + +#[macro_export] macro_rules! has { + ($T:ty: |$self:ident : $S:ty| $x:expr) => { + impl Has<$T> for $S { + fn get (&$self) -> &$T { &$x } + fn get_mut (&mut $self) -> &mut $T { &mut $x } + } + }; +} + +pub trait HasN: Send + Sync { + fn get_nth (&self, key: usize) -> &T; + fn get_nth_mut (&mut self, key: usize) -> &mut T; +} + pub trait Gettable { /// Returns current value fn get (&self) -> T; diff --git a/crates/engine/src/midi.rs b/crates/engine/src/midi.rs index 216e253c..e6a656b0 100644 --- a/crates/engine/src/midi.rs +++ b/crates/engine/src/midi.rs @@ -9,6 +9,9 @@ pub use ::midly::{ live::*, }; +mod midi_in; pub use self::midi_in::*; +mod midi_out; pub use self::midi_out::*; + /// Update notes_in array pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { match message { @@ -35,28 +38,3 @@ pub fn all_notes_off (output: &mut [Vec>]) { evt.write(&mut buf).unwrap(); output[0].push(buf); } - -/// Trait for thing that may receive MIDI. -pub trait HasMidiIns { - fn midi_ins (&self) -> &Vec; - - fn midi_ins_mut (&mut self) -> &mut Vec; - - fn has_midi_ins (&self) -> bool { - !self.midi_ins().is_empty() - } -} - -/// Trait for thing that may output MIDI. -pub trait HasMidiOuts { - fn midi_outs (&self) -> &Vec; - - fn midi_outs_mut (&mut self) -> &mut Vec; - - fn has_midi_outs (&self) -> bool { - !self.midi_outs().is_empty() - } - - /// Buffer for serializing a MIDI event. FIXME rename - fn midi_note (&mut self) -> &mut Vec; -} diff --git a/crates/engine/src/midi/midi_in.rs b/crates/engine/src/midi/midi_in.rs new file mode 100644 index 00000000..800bf2bc --- /dev/null +++ b/crates/engine/src/midi/midi_in.rs @@ -0,0 +1,42 @@ +use crate::*; + +impl>> HasMidiIns for T { + fn midi_ins (&self) -> &Vec { + self.get() + } + fn midi_ins_mut (&mut self) -> &mut Vec { + self.get_mut() + } +} + +/// 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, &[PortConnect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_ins().iter().enumerate().map(move|(i, input)|{ + let height = 1 + input.conn().len(); + let data = (i, input.name(), input.conn(), y, y + height); + y += height; + data + }) + } + fn midi_in_add (&mut self, jack: &Jack) -> Usually<()> { + let index = self.midi_ins().len(); + self.midi_ins_mut().push(JackMidiIn::new(jack, &format!("M/{index}"), &[])?); + Ok(()) + } +} + +pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; diff --git a/crates/engine/src/midi/midi_out.rs b/crates/engine/src/midi/midi_out.rs new file mode 100644 index 00000000..1bc3a74d --- /dev/null +++ b/crates/engine/src/midi/midi_out.rs @@ -0,0 +1,33 @@ +use crate::*; + +impl>> HasMidiOuts for T { + fn midi_outs (&self) -> &Vec { + self.get() + } + fn midi_outs_mut (&mut self) -> &mut Vec { + self.get_mut() + } +} + + +/// 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, &[PortConnect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_outs().iter().enumerate().map(move|(i, output)|{ + let height = 1 + output.conn().len(); + let data = (i, output.name(), output.conn(), y, y + height); + y += height; + data + }) + } + fn midi_out_add (&mut self, jack: &Jack) -> Usually<()> { + let index = self.midi_outs().len(); + self.midi_outs_mut().push(JackMidiOut::new(&jack, &format!("{index}/M"), &[])?); + Ok(()) + } +} diff --git a/deps/tengri b/deps/tengri index b45ac8f4..bad20f50 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit b45ac8f417b2f4e83e116a9ee5fe4bf3ad57a726 +Subproject commit bad20f5037dc22d572a8381840fab871ce65f565 From 6ce83fb27a60f0a2532520a39596adc253081b2d Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 14 May 2025 02:13:13 +0300 Subject: [PATCH 02/12] wip: down to 25 errors woo --- Cargo.lock | 1 + crates/app/src/model.rs | 67 +++-- crates/device/src/arranger.rs | 15 +- crates/device/src/arranger/arranger_api.rs | 213 ++++++++++++---- crates/device/src/arranger/arranger_audio.rs | 7 - crates/device/src/arranger/arranger_clip.rs | 67 ++++- crates/device/src/arranger/arranger_device.rs | 27 -- crates/device/src/arranger/arranger_edit.rs | 62 ----- crates/device/src/arranger/arranger_model.rs | 94 +++++-- crates/device/src/arranger/arranger_port.rs | 4 + crates/device/src/arranger/arranger_scene.rs | 231 +----------------- crates/device/src/arranger/arranger_scenes.rs | 65 +++++ crates/device/src/arranger/arranger_select.rs | 102 +++----- crates/device/src/arranger/arranger_track.rs | 27 +- crates/device/src/arranger/arranger_tracks.rs | 0 crates/device/src/arranger/arranger_view.rs | 222 +++++++++++------ crates/device/src/clock.rs | 14 +- crates/device/src/device.rs | 18 ++ crates/device/src/pool/pool_model.rs | 6 +- crates/device/src/sequencer/seq_model.rs | 20 +- crates/engine/Cargo.toml | 1 + crates/engine/src/jack/jack_client.rs | 6 + crates/engine/src/lib.rs | 2 +- crates/engine/src/midi/midi_in.rs | 19 +- crates/engine/src/midi/midi_out.rs | 18 +- 25 files changed, 688 insertions(+), 620 deletions(-) delete mode 100644 crates/device/src/arranger/arranger_audio.rs delete mode 100644 crates/device/src/arranger/arranger_device.rs delete mode 100644 crates/device/src/arranger/arranger_edit.rs create mode 100644 crates/device/src/arranger/arranger_scenes.rs create mode 100644 crates/device/src/arranger/arranger_tracks.rs diff --git a/Cargo.lock b/Cargo.lock index c12f9b57..253d77e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2315,6 +2315,7 @@ dependencies = [ "jack", "midly", "tengri", + "tengri_proc", "uuid", ] diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index e7411bea..18c6d61f 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -21,12 +21,12 @@ pub struct App { pub dialog: Option, /// Browses external resources, such as directories pub browser: Option, - /// Contains the currently edited musical arrangement - pub arranger: Option, /// Contains all clips in the project pub pool: Option, /// Contains the currently edited MIDI clip pub editor: Option, + /// Contains the currently edited musical arrangement + pub arranger: Arrangement, // Cache of formatted strings pub view_cache: Arc>, @@ -34,6 +34,12 @@ pub struct App { pub color: ItemTheme, } +has!(Option: |self: App|self.arrangement.selected); +has!(Vec: |self: App|self.arrangement.midi_ins); +has!(Vec: |self: App|self.arrangement.midi_outs); +has!(Vec: |self: App|self.arrangement.scenes); +has!(Vec: |self: App|self.arrangement.tracks); + impl App { pub fn toggle_dialog (&mut self, dialog: Option) { self.dialog = if self.dialog == dialog { @@ -86,16 +92,10 @@ pub enum Message { FailedToAddDevice, } -content!(TuiOut: |self: Message| match self { - Self::FailedToAddDevice => "Failed to add device." -}); - +content!(TuiOut: |self: Message| match self { Self::FailedToAddDevice => "Failed to add device." }); has_size!(|self: App|&self.size); - -has_clock!(|self: App|self.clock); - +has!(Clock: |self: App|self.clock); has_clips!(|self: App|self.pool.as_ref().expect("no clip pool").clips); - has_editor!(|self: App|{ editor = self.editor; editor_w = { @@ -109,30 +109,6 @@ has_editor!(|self: App|{ is_editing = self.editing.load(Relaxed); }); -impl HasTracks for App { - fn midi_ins (&self) -> &Vec { - &self.arranger.midi_ins - } - fn midi_outs (&self) -> &Vec { - &self.arranger.midi_outs - } - fn tracks (&self) -> &Vec { - &self.arranger.tracks - } - fn tracks_mut (&mut self) -> &mut Vec { - &mut self.arranger.tracks - } -} - -impl HasScenes for Arrangement { - fn scenes (&self) -> &Vec { - &self.arranger.scenes - } - fn scenes_mut (&mut self) -> &mut Vec { - &mut self.arranger.scenes - } -}if - #[tengri_proc::expose] impl App { fn _todo_isize_stub (&self) -> isize { @@ -258,4 +234,27 @@ impl App { 0 } } + fn device_add_lv2 (&mut self) -> Usually<()> { + todo!(); + Ok(()) + } + fn device_add_sampler (&mut self, jack: &Jack) -> Usually { + let name = jack.with_client(|c|c.name().to_string()); + let midi = self.track().expect("no active track").sequencer.midi_outs[0].name(); + let sampler = if let Ok(sampler) = Sampler::new( + jack, + &format!("{}/Sampler", Has::::get(self).name), + &[PortConnect::exact(format!("{name}:{midi}"))], + &[&[], &[]], + &[&[], &[]] + ) { + self.dialog = None; + Device::Sampler(sampler) + } else { + self.dialog = Some(Dialog::Message(Message::FailedToAddDevice)); + return Err("failed to add device".into()) + }; + Has::::get_mut(self).expect("no active track").devices.push(sampler); + Ok(()) + } } diff --git a/crates/device/src/arranger.rs b/crates/device/src/arranger.rs index 396aebe7..2939b23e 100644 --- a/crates/device/src/arranger.rs +++ b/crates/device/src/arranger.rs @@ -10,12 +10,13 @@ macro_rules! def_sizes_iter { mod arranger_api; pub use self::arranger_api::*; mod arranger_clip; pub use self::arranger_clip::*; -mod arranger_device; pub use self::arranger_device::*; -mod arranger_port; pub use self::arranger_port::*; mod arranger_model; pub use self::arranger_model::*; +mod arranger_port; pub use self::arranger_port::*; mod arranger_scene; pub use self::arranger_scene::*; +mod arranger_scenes; pub use self::arranger_scenes::*; mod arranger_select; pub use self::arranger_select::*; mod arranger_track; pub use self::arranger_track::*; +mod arranger_tracks; pub use self::arranger_tracks::*; mod arranger_view; pub use self::arranger_view::*; def_sizes_iter!(ScenesSizes => Scene); @@ -74,13 +75,3 @@ impl HasWidth for Track { } } } - -pub trait Has: Send + Sync { - fn get (&self) -> &T; - fn get_mut (&mut self) -> &mut T; -} - -pub trait HasMany: Send + Sync { - fn get (&self, key: U) -> &T; - fn get_mut (&mut self, key: U) -> &mut T; -} diff --git a/crates/device/src/arranger/arranger_api.rs b/crates/device/src/arranger/arranger_api.rs index d13d3885..bd6d4721 100644 --- a/crates/device/src/arranger/arranger_api.rs +++ b/crates/device/src/arranger/arranger_api.rs @@ -1,7 +1,83 @@ use crate::*; +#[tengri_proc::expose] +impl Arrangement { + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } +} + #[tengri_proc::command(Arrangement)] impl ArrangementCommand { + /// Set the selection + fn select (arranger: &mut Arrangement, s: Option) -> Perhaps { + arranger.selected = s; + // autoedit: load focused clip in editor. + if let Some(ref mut editor) = arranger.editor { + editor.set_clip(match arranger.selected { + Some(Selection::TrackClip { track, scene }) + if let Some(Some(Some(clip))) = arranger + .scenes.get(scene) + .map(|s|s.clips.get(track)) => Some(clip), + _ => None + }); + } + Ok(None) + } + /// Launch a clip or scene + fn launch (arranger: &mut Arrangement) -> Perhaps { + use Selection::*; + match arranger.selected { + Some(Track(t)) => { + arranger.tracks[t].sequencer.enqueue_next(None) + }, + Some(TrackClip { track, scene }) => { + arranger.tracks[track].sequencer.enqueue_next(arranger.scenes[scene].clips[track].as_ref()) + }, + Some(Scene(s)) => { + for t in 0..arranger.tracks.len() { + arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref()) + } + }, + _ => {} + }; + Ok(None) + } + /// Set the color of the selected entity + fn set_color (arranger: &mut Arrangement, palette: Option) -> Option { + use Selection::*; + let palette = palette.unwrap_or_else(||ItemTheme::random()); + Some(match arranger.selected { + Some(Mix) => { + let old = arranger.color; + arranger.color = palette; + old + }, + Some(Scene(s)) => { + let old = arranger.scenes[s].color; + arranger.scenes[s].color = palette; + old + } + Some(Track(t)) => { + let old = arranger.tracks[t].color; + arranger.tracks[t].color = palette; + old + } + Some(TrackClip { track, scene }) => { + if let Some(ref clip) = arranger.scenes[scene].clips[track] { + let mut clip = clip.write().unwrap(); + let old = clip.color; + clip.color = palette; + old + } else { + return None + } + }, + _ => todo!() + }) + } + fn track (arranger: &mut Arrangement, track: TrackCommand) -> Perhaps { todo!("delegate") } @@ -9,9 +85,9 @@ impl ArrangementCommand { let index = arranger.track_add(None, None, &[], &[])?.0; arranger.selected = match arranger.selected { Some(Selection::Track(_)) => - Selection::Track(index), + Some(Selection::Track(index)), Some(Selection::TrackClip { track, scene }) => - Selection::TrackClip { track: index, scene }, + Some(Selection::TrackClip { track: index, scene }), _ => arranger.selected }; Ok(Some(Self::TrackDelete { index })) @@ -38,14 +114,14 @@ impl ArrangementCommand { Ok(None) //TODO:Ok(Some(Self::TrackAdd ( index, track: Some(deleted_track) }) } - fn midi_in (arranger: &mut Arrangement, input: InputCommand) -> Perhaps { + fn midi_in (arranger: &mut Arrangement, input: MidiInputCommand) -> Perhaps { todo!("delegate") } fn midi_in_add (arranger: &mut Arrangement) -> Perhaps { arranger.midi_in_add()?; Ok(None) } - fn midi_out (arranger: &mut Arrangement, input: OutputCommand) -> Perhaps { + fn midi_out (arranger: &mut Arrangement, input: MidiOutputCommand) -> Perhaps { todo!("delegate") } fn midi_out_add (arranger: &mut Arrangement) -> Perhaps { @@ -65,10 +141,10 @@ impl ArrangementCommand { fn scene_add (arranger: &mut Arrangement) -> Perhaps { let index = arranger.scene_add(None, None)?.0; arranger.selected = match arranger.selected { - Selection::Scene(_) => - Selection::Scene(index), - Selection::TrackClip { track, scene } => - Selection::TrackClip { track, scene: index }, + Some(Selection::Scene(_)) => + Some(Selection::Scene(index)), + Some(Selection::TrackClip { track, scene }) => + Some(Selection::TrackClip { track, scene: index }), _ => arranger.selected }; Ok(None) // TODO @@ -80,7 +156,8 @@ impl ArrangementCommand { fn scene_delete (arranger: &mut Arrangement, index: usize) -> Perhaps { let scenes = arranger.scenes_mut(); Ok(if scenes.get(index).is_some() { - Some(scenes.remove(index)) + let _scene = scenes.remove(index); + None } else { None }) @@ -90,6 +167,7 @@ impl ArrangementCommand { let clip = arranger.scenes[index].clips[track].as_ref(); arranger.tracks[track].sequencer.enqueue_next(clip); } + Ok(None) } fn clip (arranger: &mut Arrangement, scene: ClipCommand) -> Perhaps { todo!("delegate") @@ -120,52 +198,20 @@ impl ArrangementCommand { //("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap()))) todo!() } - } -#[tengri_proc::command(MidiClip)] -impl ClipCommand { - fn set_color (clip: &mut MidiClip, color: Option) -> Perhaps { - //(SetColor [t: usize, s: usize, c: ItemTheme] - //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); - //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) - todo!() - } - fn set_loop (clip: &mut MidiClip, looping: Option) -> Perhaps { - //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) - //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) - todo!() +impl<'state> Context<'state, TrackCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { + Context::get(&self, iter) } } -#[tengri_proc::command(Device)] -impl DeviceCommand { -} - -#[tengri_proc::command(JackMidiIn)] -impl InputCommand { -} - -#[tengri_proc::command(JackMidiOut)] -impl OutputCommand { -} - -#[tengri_proc::command(Scene)] -impl SceneCommand { - fn set_name (scene: &mut Scene, mut name: Arc) -> Perhaps { - std::mem::swap(&mut scene.name, &mut name); - Ok(Some(Self::SetName { name })) - } - fn set_color (scene: &mut Scene, mut color: ItemTheme) -> Perhaps { - std::mem::swap(&mut scene.color, &mut color); - Ok(Some(Self::SetColor { color })) - } - fn set_size (scene: &mut Scene, size: usize) -> Perhaps { - todo!() - } - fn set_zoom (scene: &mut Scene, zoom: usize) -> Perhaps { - todo!() - } +#[tengri_proc::expose] +impl Track { + 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!() } } #[tengri_proc::command(Track)] @@ -174,8 +220,8 @@ impl TrackCommand { std::mem::swap(&mut name, &mut track.name); Ok(Some(Self::SetName { name })) } - fn set_color (track: &mut Track, color: ItemTheme) -> Perhaps { - std::mem::swap(color, track.color); + fn set_color (track: &mut Track, mut color: ItemTheme) -> Perhaps { + std::mem::swap(&mut color, &mut track.color); Ok(Some(Self::SetColor { color })) } fn set_mute (track: &mut Track, value: Option) -> Perhaps { @@ -205,3 +251,66 @@ impl TrackCommand { Ok(None) } } + +impl<'state> Context<'state, SceneCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { + Context::get(&self, iter) + } +} + +impl<'state> Context<'state, ClipCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { + Context::get(&self, iter) + } +} + +#[tengri_proc::expose] +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!() } +} + +#[tengri_proc::command(Scene)] +impl SceneCommand { + fn set_name (scene: &mut Scene, mut name: Arc) -> Perhaps { + std::mem::swap(&mut scene.name, &mut name); + Ok(Some(Self::SetName { name })) + } + fn set_color (scene: &mut Scene, mut color: ItemTheme) -> Perhaps { + std::mem::swap(&mut scene.color, &mut color); + Ok(Some(Self::SetColor { color })) + } + fn set_size (scene: &mut Scene, size: usize) -> Perhaps { + todo!() + } + fn set_zoom (scene: &mut Scene, zoom: usize) -> Perhaps { + todo!() + } +} + +#[tengri_proc::expose] +impl MidiClip { + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_bool_stub_ (&self) -> bool { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } +} + +#[tengri_proc::command(MidiClip)] +impl ClipCommand { + fn set_color (clip: &mut MidiClip, color: Option) -> Perhaps { + //(SetColor [t: usize, s: usize, c: ItemTheme] + //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); + //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) + todo!() + } + fn set_loop (clip: &mut MidiClip, looping: Option) -> Perhaps { + //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) + //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) + todo!() + } +} diff --git a/crates/device/src/arranger/arranger_audio.rs b/crates/device/src/arranger/arranger_audio.rs deleted file mode 100644 index 71139cf5..00000000 --- a/crates/device/src/arranger/arranger_audio.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::*; - -pub trait RenderTracks { -} - -impl Audio for T { -} diff --git a/crates/device/src/arranger/arranger_clip.rs b/crates/device/src/arranger/arranger_clip.rs index ba5fe2bc..b5acd217 100644 --- a/crates/device/src/arranger/arranger_clip.rs +++ b/crates/device/src/arranger/arranger_clip.rs @@ -1,7 +1,7 @@ use crate::*; - impl Arrangement { + /// Put a clip in a slot pub(crate) fn clip_put ( &mut self, track: usize, scene: usize, clip: Option>> @@ -10,6 +10,7 @@ impl Arrangement { self.scenes[scene].clips[track] = clip; old } + /// Change the color of a clip, returning the previous one pub(crate) fn clip_set_color ( &self, track: usize, scene: usize, color: ItemTheme @@ -22,14 +23,78 @@ impl Arrangement { old }) } + /// Get the active clip pub(crate) fn clip (&self) -> Option>> { self.scene()?.clips.get(self.selected().track()?)?.clone() } + /// Toggle looping for the active clip pub(crate) fn toggle_loop (&mut self) { if let Some(clip) = self.clip() { clip.write().unwrap().toggle_loop() } } + +} + +#[cfg(all(feature = "pool", feature = "editor"))] +pub trait AutoCreate: + Has> + + Has> + + Has> + + Has> + + Has> + + Send + Sync +{ + // Create new clip in pool when entering empty cell + fn clip_auto_create (&mut self) -> Option>> { + if let Some(pool) = Has::>::get(self) + && let Some(Selection::TrackClip { track, scene }) = self.get() + && let Some(scene) = Has::>::get_mut(self).get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && slot.is_none() + && let Some(track) = Has::>::get_mut(self).get_mut(track) + { + let (index, mut clip) = pool.add_new_clip(); + // autocolor: new clip colors from scene and track color + let color = track.color.base.mix(scene.color.base, 0.5); + clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); + if let Some(ref mut editor) = Has::>::get_mut(self) { + editor.set_clip(Some(&clip)); + } + *slot = Some(clip.clone()); + Some(clip) + } else { + None + } + } +} + +#[cfg(all(feature = "pool", feature = "editor"))] +pub trait AutoRemove: + Has> + + Has> + + Has> + + Has> + + Has> + + Send + Sync +{ + // Remove clip from arrangement when exiting empty clip editor + fn clip_auto_remove (&mut self) { + if let Some(ref mut pool) = Has::>::get(self) + && let Some(Selection::TrackClip { track, scene }) = self.get() + && let Some(scene) = Has::>::get_mut(self).get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && let Some(clip) = slot.as_mut() + { + let mut swapped = None; + if clip.read().unwrap().count_midi_messages() == 0 { + std::mem::swap(&mut swapped, slot); + } + if let Some(clip) = swapped { + pool.delete_clip(&clip.read().unwrap()); + } + } + } } diff --git a/crates/device/src/arranger/arranger_device.rs b/crates/device/src/arranger/arranger_device.rs deleted file mode 100644 index 60894ab5..00000000 --- a/crates/device/src/arranger/arranger_device.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::*; - -impl Arrangement { - fn device_add_lv2 (&mut self) -> Usually<()> { - todo!(); - Ok(()) - } - fn device_add_sampler (&mut self) -> Usually<()> { - let name = self.jack.with_client(|c|c.name().to_string()); - let midi = self.track().expect("no active track").sequencer.midi_outs[0].name(); - let sampler = if let Ok(sampler) = Sampler::new( - &self.jack, - &format!("{}/Sampler", &self.track().expect("no active track").name), - &[PortConnect::exact(format!("{name}:{midi}"))], - &[&[], &[]], - &[&[], &[]] - ) { - self.dialog = None; - Device::Sampler(sampler) - } else { - self.dialog = Some(Dialog::Message(Message::FailedToAddDevice)); - return Err("failed to add device".into()) - }; - self.track_mut().expect("no active track").devices.push(sampler); - Ok(()) - } -} diff --git a/crates/device/src/arranger/arranger_edit.rs b/crates/device/src/arranger/arranger_edit.rs deleted file mode 100644 index 715623f1..00000000 --- a/crates/device/src/arranger/arranger_edit.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::*; - -#[cfg(all(feature = "pool", feature = "editor"))] -pub trait AutoCreate: - Has> + - Has> + - Has> + - Has> + - Has> + - Send + Sync -{ - // Create new clip in pool when entering empty cell - fn clip_auto_create (&mut self) -> Option>> { - if let Some(pool) = Has::::get(self) - && let Some(Selection::TrackClip { track, scene }) = self.get() - && let Some(scene) = Has::>::get_mut(self).get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && slot.is_none() - && let Some(track) = Has::>::get_mut(self).get_mut(track) - { - let (index, mut clip) = pool.add_new_clip(); - // autocolor: new clip colors from scene and track color - let color = track.color.base.mix(scene.color.base, 0.5); - clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); - if let Some(ref mut editor) = Has::>::get_mut(self) { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip.clone()); - Some(clip) - } else { - None - } - } -} - -#[cfg(all(feature = "pool", feature = "editor"))] -pub trait AutoRemove: - Has> + - Has> + - Has> + - Has> + - Has> + - Send + Sync -{ - // Remove clip from arrangement when exiting empty clip editor - fn clip_auto_remove (&mut self) { - if let Some(ref mut pool) = Has::::get(self) - && let Some(Selection::TrackClip { track, scene }) = self.get() - && let Some(scene) = Has::>::get_mut(self).get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && let Some(clip) = slot.as_mut() - { - let mut swapped = None; - if clip.read().unwrap().count_midi_messages() == 0 { - std::mem::swap(&mut swapped, slot); - } - if let Some(clip) = swapped { - pool.delete_clip(&clip.read().unwrap()); - } - } - } -} diff --git a/crates/device/src/arranger/arranger_model.rs b/crates/device/src/arranger/arranger_model.rs index 0be3e78b..e12e73d1 100644 --- a/crates/device/src/arranger/arranger_model.rs +++ b/crates/device/src/arranger/arranger_model.rs @@ -35,6 +35,8 @@ pub struct Arrangement { pub arranger: Arc>, /// Display size pub size: Measure, + /// Jack client handle + pub jack: Jack, } has!(Option: |self: Arrangement|self.selected); @@ -42,33 +44,73 @@ has!(Vec: |self: Arrangement|self.midi_ins); has!(Vec: |self: Arrangement|self.midi_outs); has!(Vec: |self: Arrangement|self.scenes); has!(Vec: |self: Arrangement|self.tracks); +has!(Jack: |self: Arrangement|self.jack); -impl Has> for Arrangement { - fn get (&self) -> &Option { - Has::::get(self) - .and_then(|selection|selection.track()) - .and_then(|index|Has::>::get(self).get(index)) - .flatten() +impl Arrangement { + /// Width of display + pub(crate) fn w (&self) -> u16 { + self.size.w() as u16 } - fn get_mut (&mut self) -> &mut Option { - Has::::get(self) - .and_then(|selection|selection.track()) - .and_then(|index|Has::>::get_mut(self).get_mut(index)) - .flatten() - } -} - -impl Has> for Arrangement { - fn get (&self) -> &Option { - Has::::get(self) - .and_then(|selection|selection.track()) - .and_then(|index|Has::>::get(self).get(index)) - .flatten() - } - fn get_mut (&mut self) -> &mut Option { - Has::::get(self) - .and_then(|selection|selection.track()) - .and_then(|index|Has::>::get_mut(self).get_mut(index)) - .flatten() + /// Width allocated for sidebar. + pub(crate) fn w_sidebar (&self, is_editing: bool) -> u16 { + self.w() / if is_editing { 16 } else { 8 } as u16 + } + /// Width taken by all tracks. + pub(crate) fn w_tracks (&self) -> u16 { + self.tracks_with_sizes().last().map(|(_, _, _, x)|x as u16).unwrap_or(0) + } + /// Width available to display tracks. + pub(crate) fn w_tracks_area (&self, is_editing: bool) -> u16 { + self.w().saturating_sub(2 * self.w_sidebar(is_editing)) + } + /// Height of display + pub(crate) fn h (&self) -> u16 { + self.size.h() as u16 + } + /// Height available to display track headers. + pub(crate) fn h_tracks_area (&self) -> u16 { + 5 // FIXME + //self.h().saturating_sub(self.h_inputs() + self.h_outputs()) + } + /// Height available to display tracks. + pub(crate) fn h_scenes_area (&self) -> u16 { + //15 + self.h().saturating_sub( + self.h_inputs() + + self.h_outputs() + + self.h_devices() + + 13 // FIXME + ) + } + /// Height taken by all scenes. + pub(crate) fn h_scenes (&self, is_editing: bool) -> u16 { + let (selected_track, selected_scene) = match Has::>::get(self) { + Some(Selection::Track(t)) => (Some(*t), None), + Some(Selection::Scene(s)) => (None, Some(*s)), + Some(Selection::TrackClip { track, scene }) => (Some(*track), Some(*scene)), + _ => (None, None) + }; + self.scenes_with_sizes( + is_editing, + ArrangerView::H_SCENE, + ArrangerView::H_EDITOR, + selected_track, + selected_scene + ) + .last() + .map(|(_, _, _, y)|y as u16).unwrap_or(0) + } + /// Height taken by all inputs. + pub(crate) fn h_inputs (&self) -> u16 { + self.midi_ins_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) + } + /// Height taken by all outputs. + pub(crate) fn h_outputs (&self) -> u16 { + self.midi_outs_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) + } + /// Height taken by visible device slots. + pub(crate) fn h_devices (&self) -> u16 { + 2 + //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } } diff --git a/crates/device/src/arranger/arranger_port.rs b/crates/device/src/arranger/arranger_port.rs index 9b09ba84..c98b0807 100644 --- a/crates/device/src/arranger/arranger_port.rs +++ b/crates/device/src/arranger/arranger_port.rs @@ -54,6 +54,10 @@ impl<'a> ArrangerView<'a> { |_, _|Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ "))))) } +} + +impl<'a> ArrangerView<'a> { + pub(crate) fn output_nexts (&self) -> impl Content + '_ { Tryptich::top(2) .left(self.width_side, Align::ne("From clip:")) diff --git a/crates/device/src/arranger/arranger_scene.rs b/crates/device/src/arranger/arranger_scene.rs index 6fb3401c..a217d43e 100644 --- a/crates/device/src/arranger/arranger_scene.rs +++ b/crates/device/src/arranger/arranger_scene.rs @@ -1,5 +1,16 @@ use crate::*; +impl> + Send + Sync> HasScene for T {} + +pub trait HasScene: Has> + Send + Sync { + fn scene (&self) -> &Option { + Has::>::get(self) + } + fn scene_mut (&mut self) -> &mut Option { + Has::>::get_mut(self) + } +} + #[derive(Debug, Default)] pub struct Scene { /// Name of scene @@ -39,223 +50,3 @@ impl Scene { match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } } } - -impl> + Send + Sync> HasScenes for T {} - -pub trait HasScenes: Has> + Send + Sync { - fn scenes (&self) -> &Vec { - Has::>::get(self) - } - fn scenes_mut (&self) -> &mut Vec { - Has::>::get_mut(self) - } - fn scenes_with_sizes ( - &self, - editing: bool, - height: usize, - larger: usize, - selected_track: Option, - selected_scene: Option, - ) -> impl ScenesSizes<'_> { - let mut y = 0; - self.scenes().iter().enumerate().map(move|(s, scene)|{ - let active = editing && selected_track.is_some() && selected_scene == Some(s); - let height = if active { larger } else { height }; - let data = (s, scene, y, y + height); - y += height; - data - }) - } - /// Generate the default name for a new scene - fn scene_default_name (&self) -> Arc { - format!("Sc{:3>}", self.scenes().len() + 1).into() - } - fn scene_longest_name (&self) -> usize { - self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) - } -} - -impl> + Send + Sync> HasScene for T {} - -pub trait HasScene: Has> + Send + Sync { - fn scene (&self) -> Option<&Scene> { - Has::>::get(self) - } - fn scene_mut (&mut self) -> Option<&mut Scene> { - Has::>::get_mut(self) - } -} - -impl<'a> ArrangerView<'a> { - /// Default scene height. - pub(crate) const H_SCENE: usize = 2; - /// Default editor height. - pub(crate) const H_EDITOR: usize = 15; - - /// Render scenes with clips - pub(crate) fn scenes (&'a self, editor: Option) -> impl Content + 'a { - - /// A scene with size and color. - type SceneWithColor<'a> = (usize, &'a Scene, usize, usize, Option); - - let Self { - arrangement, - width, width_side, width_mid, - scenes_height, scene_last, scene_selected, - track_selected, is_editing, .. - } = self; - - let selection = Has::>::get(*arrangement); - let (selected_track, selected_scene) = match selection { - Some(Selection::Track(t)) => (Some(*t), None), - Some(Selection::Scene(s)) => (None, Some(*s)), - Some(Selection::TrackClip { track, scene }) => (Some(*track), Some(*scene)), - _ => (None, None) - }; - - let scenes_with_scene_colors = ||arrangement.scenes_with_sizes( - *is_editing, - Self::H_SCENE, - Self::H_EDITOR, - selected_track, - selected_scene, - ).map_while(|(s, scene, y1, y2)|if y2 as u16 > *scenes_height { - None - } else { - Some((s, scene, y1, y2, if s == 0 { - None - } else { - Some(arrangement.scenes()[s-1].color) - })) - }); - - let scene_header = |(s, scene, y1, y2, previous): SceneWithColor, _|{ - let height = (1 + y2 - y1) as u16; - let name = Some(scene.name.clone()); - let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⯈ ", name)))); - let same_track = true; - let selected = same_track && *scene_selected == Some(s); - let neighbor = same_track && s > 0 && *scene_selected == Some(s - 1); - let is_last = *scene_last == s; - let theme = scene.color; - let fg = theme.lightest.rgb; - let bg = if selected { theme.light } else { theme.base }.rgb; - let hi = if let Some(previous) = previous { - if neighbor { - previous.light.rgb - } else { - previous.base.rgb - } - } else { - Reset - }; - let lo = if is_last { - Reset - } else if selected { - theme.light.rgb - } else { - theme.base.rgb - }; - Fill::x(map_south(y1 as u16, height, Fixed::y(height, Phat { - width: 0, height: 0, content, colors: [fg, bg, hi, lo] - }))) - }; - - let scenes_with_track_colors = |track: usize| arrangement.scenes_with_sizes( - self.is_editing, - Self::H_SCENE, - Self::H_EDITOR, - selected_track, - selected_scene, - ).map_while(|(s, scene, y1, y2)|if y2 as u16 > self.scenes_height { - None - } else { - Some((s, scene, y1, y2, if s == 0 { - None - } else { - Some(self.arrangement.scenes[s-1].clips[track].as_ref() - .map(|c|c.read().unwrap().color) - .unwrap_or(ItemTheme::G[32])) - })) - }); - - let scene_track_clips = |track_index, track|Map::new( - ||scenes_with_track_colors(track_index), - |(s, scene, y1, y2, previous): SceneWithColor<'a>, _|{ - let (name, theme) = if let Some(clip) = &scene.clips[track_index] { - let clip = clip.read().unwrap(); - (Some(clip.name.clone()), clip.color) - } else { - (None, ItemTheme::G[32]) - }; - let height = (1 + y2 - y1) as u16; - let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⏹ ", name)))); - let same_track = *track_selected == Some(track_index); - let selected = same_track && *scene_selected == Some(s); - let neighbor = same_track && s > 0 && *scene_selected == Some(s - 1); - let is_last = *scene_last == s; - let fg = theme.lightest.rgb; - let bg = if selected { theme.light } else { theme.base }.rgb; - let hi = if let Some(previous) = previous { - if neighbor { - previous.light.rgb - } else { - previous.base.rgb - } - } else { - Reset - }; - let lo = if is_last { - Reset - } else if selected { - theme.light.rgb - } else { - theme.base.rgb - }; - map_south(y1 as u16, height, Bsp::b(Fixed::y(height, Phat { - width: 0, height: 0, content, colors: [fg, bg, hi, lo] - }), When( - *is_editing && same_track && *scene_selected == Some(s), - editor - ))) - }); - - Tryptich::center(*scenes_height) - .left(*width_side, Map::new( - ||scenes_with_scene_colors(), - scene_header)) - .middle(*width_mid, per_track( - *width_mid, - ||self.tracks_with_sizes_scrolled(), - scene_track_clips)) - } - -} - -impl Arrangement { - /// Add multiple scenes - pub fn scenes_add (&mut self, n: usize) -> Usually<()> { - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..n { - let _ = self.scene_add(None, Some( - scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() - ))?; - } - Ok(()) - } - - /// Add a scene - pub fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<(usize, &mut Scene)> - { - let scene = Scene { - name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), - clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(ItemTheme::random), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok((index, &mut self.scenes_mut()[index])) - } -} diff --git a/crates/device/src/arranger/arranger_scenes.rs b/crates/device/src/arranger/arranger_scenes.rs new file mode 100644 index 00000000..8603e603 --- /dev/null +++ b/crates/device/src/arranger/arranger_scenes.rs @@ -0,0 +1,65 @@ +use crate::*; + +impl> + Send + Sync> HasScenes for T {} + +pub trait HasScenes: Has> + Send + Sync { + fn scenes (&self) -> &Vec { + Has::>::get(self) + } + fn scenes_mut (&mut self) -> &mut Vec { + Has::>::get_mut(self) + } + fn scenes_with_sizes ( + &self, + editing: bool, + height: usize, + larger: usize, + selected_track: Option, + selected_scene: Option, + ) -> impl ScenesSizes<'_> { + let mut y = 0; + self.scenes().iter().enumerate().map(move|(s, scene)|{ + let active = editing && selected_track.is_some() && selected_scene == Some(s); + let height = if active { larger } else { height }; + let data = (s, scene, y, y + height); + y += height; + data + }) + } + /// Generate the default name for a new scene + fn scene_default_name (&self) -> Arc { + format!("Sc{:3>}", self.scenes().len() + 1).into() + } + fn scene_longest_name (&self) -> usize { + self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) + } +} + +impl AddScene for T {} + +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])) + } +} diff --git a/crates/device/src/arranger/arranger_select.rs b/crates/device/src/arranger/arranger_select.rs index 6105c200..2ef03033 100644 --- a/crates/device/src/arranger/arranger_select.rs +++ b/crates/device/src/arranger/arranger_select.rs @@ -1,5 +1,42 @@ use crate::*; +impl> HasSelection for T {} + +pub trait HasSelection: Has { + fn selection (&self) -> &Selection { + self.get() + } + fn selection_mut (&mut self) -> &mut Selection { + self.get_mut() + } +} + +impl Has> for Arrangement { + fn get (&self) -> &Option { + Has::>::get(self) + .and_then(|selection|selection.track()) + .and_then(|index|Has::>::get(self).get(index)) + } + fn get_mut (&mut self) -> &mut Option { + Has::>::get(self) + .and_then(|selection|selection.track()) + .and_then(|index|Has::>::get_mut(self).get_mut(index)) + } +} + +impl Has> for Arrangement { + fn get (&self) -> &Option { + Has::>::get(self) + .and_then(|selection|selection.track()) + .and_then(|index|Has::>::get(self).get(index)) + } + fn get_mut (&mut self) -> &mut Option { + Has::>::get(self) + .and_then(|selection|selection.track()) + .and_then(|index|Has::>::get_mut(self).get_mut(index)) + } +} + /// Represents the current user selection in the arranger #[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { @@ -147,69 +184,4 @@ impl Selection { } impl Arrangement { - /// Set the selection - pub(crate) fn select (&mut self, s: Selection) { - self.selected = s; - // autoedit: load focused clip in editor. - if let Some(ref mut editor) = self.editor { - editor.set_clip(match self.selected { - Selection::TrackClip { track, scene } - if let Some(Some(Some(clip))) = self - .scenes.get(scene) - .map(|s|s.clips.get(track)) => Some(clip), - _ => None - }); - } - } - /// Launch a clip or scene - pub(crate) fn launch (&mut self) { - use Selection::*; - match self.selected { - Track(t) => { - self.tracks[t].sequencer.enqueue_next(None) - }, - TrackClip { track, scene } => { - self.tracks[track].sequencer.enqueue_next(self.scenes[scene].clips[track].as_ref()) - }, - Scene(s) => { - for t in 0..self.tracks.len() { - self.tracks[t].sequencer.enqueue_next(self.scenes[s].clips[t].as_ref()) - } - }, - _ => {} - }; - } - /// Set the color of the selected entity - pub fn set_color (&mut self, palette: Option) -> Option { - use Selection::*; - let palette = palette.unwrap_or_else(||ItemTheme::random()); - Some(match self.selected { - Mix => { - let old = self.color; - self.color = palette; - old - }, - Scene(s) => { - let old = self.scenes[s].color; - self.scenes[s].color = palette; - old - } - Track(t) => { - let old = self.tracks[t].color; - self.tracks[t].color = palette; - old - } - TrackClip { track, scene } => { - if let Some(ref clip) = self.scenes[scene].clips[track] { - let mut clip = clip.write().unwrap(); - let old = clip.color; - clip.color = palette; - old - } else { - return None - } - }, - _ => todo!() - }) - } } diff --git a/crates/device/src/arranger/arranger_track.rs b/crates/device/src/arranger/arranger_track.rs index fc028ee5..41cb5bf1 100644 --- a/crates/device/src/arranger/arranger_track.rs +++ b/crates/device/src/arranger/arranger_track.rs @@ -13,8 +13,9 @@ pub struct Track { /// Device chain pub devices: Vec, } -has_clock!(|self: Track|self.sequencer.clock); -has_sequencer!(|self: Track|self.sequencer); + +has!(Clock: |self: Track|self.sequencer.clock); +has!(Sequencer: |self: Track|self.sequencer); impl Track { /// Create a new track with only the default [Sequencer]. @@ -98,7 +99,7 @@ impl Track { impl> + Send + Sync> HasTracks for T {} -pub trait HasTracks: Send + Sync { +pub trait HasTracks: Has> + Send + Sync { fn tracks (&self) -> &Vec { Has::>::get(self) } @@ -107,7 +108,7 @@ pub trait HasTracks: Send + Sync { } /// Run audio callbacks for every track and every device fn tracks_render (&mut self, client: &Client, scope: &ProcessScope) -> Control { - for track in self.tracks.iter_mut() { + for track in self.tracks_mut().iter_mut() { if Control::Quit == PlayerAudio( track.sequencer_mut(), &mut self.note_buf, &mut self.midi_buf ).process(client, scope) { @@ -131,7 +132,7 @@ pub trait HasTracks: Send + Sync { } } /// Stop all playing clips - fn tracks_launch (&mut self, clips: Option>>>) { + 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); @@ -170,11 +171,11 @@ pub trait HasTracks: Send + Sync { impl> + Send + Sync> HasTrack for T {} pub trait HasTrack: Has> + Send + Sync { - fn scene (&self) -> Option<&Track> { - Has::>::get(self) + fn track (&self) -> Option<&Track> { + Has::>::get(self).as_ref() } - fn scene_mut (&mut self) -> Option<&mut Track> { - Has::>::get_mut(self) + fn track_mut (&mut self) -> Option<&mut Track> { + Has::>::get_mut(self).as_mut() } } @@ -216,9 +217,11 @@ pub(crate) fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( ) } -impl Arrangement { +impl AddTrack for T {} + +pub trait AddTrack: HasTracks + HasScenes + HasClock + HasJack { /// Add multiple tracks - pub fn tracks_add ( + fn tracks_add ( &mut self, count: usize, width: Option, @@ -239,7 +242,7 @@ impl Arrangement { } /// Add a track - pub fn track_add ( + fn track_add ( &mut self, name: Option<&str>, color: Option, diff --git a/crates/device/src/arranger/arranger_tracks.rs b/crates/device/src/arranger/arranger_tracks.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs index f94b89aa..e28d3362 100644 --- a/crates/device/src/arranger/arranger_view.rs +++ b/crates/device/src/arranger/arranger_view.rs @@ -1,78 +1,5 @@ use crate::*; -impl Arrangement { - /// Width of display - pub(crate) fn w (&self) -> u16 { - self.size.w() as u16 - } - /// Width allocated for sidebar. - pub(crate) fn w_sidebar (&self) -> u16 { - self.w() / if self.is_editing() { 16 } else { 8 } as u16 - } - /// Width taken by all tracks. - pub(crate) fn w_tracks (&self) -> u16 { - self.tracks_with_sizes().last().map(|(_, _, _, x)|x as u16).unwrap_or(0) - } - /// Width available to display tracks. - pub(crate) fn w_tracks_area (&self) -> u16 { - self.w().saturating_sub(2 * self.w_sidebar()) - } - /// Height of display - pub(crate) fn h (&self) -> u16 { - self.size.h() as u16 - } - /// Height available to display track headers. - pub(crate) fn h_tracks_area (&self) -> u16 { - 5 // FIXME - //self.h().saturating_sub(self.h_inputs() + self.h_outputs()) - } - /// Height available to display tracks. - pub(crate) fn h_scenes_area (&self) -> u16 { - //15 - self.h().saturating_sub( - self.h_inputs() + - self.h_outputs() + - self.h_devices() + - 13 // FIXME - ) - } - /// Height taken by all scenes. - pub(crate) fn h_scenes (&self) -> u16 { - let (selected_track, selected_scene) = match Has::>::get(self) { - Some(Selection::Track(t)) => (Some(*t), None), - Some(Selection::Scene(s)) => (None, Some(*s)), - Some(Selection::TrackClip { track, scene }) => (Some(*track), Some(*scene)), - _ => (None, None) - }; - self.scenes_with_sizes( - self.is_editing, - ArrangerView::H_SCENE, - ArrangerView::H_EDITOR, - selected_track, - selected_scene - ) - .last() - .map(|(_, _, _, y)|y as u16).unwrap_or(0) - } - /// Height taken by all inputs. - pub(crate) fn h_inputs (&self) -> u16 { - self.inputs_with_sizes() - .last() - .map(|(_, _, _, _, y)|y as u16).unwrap_or(0) - } - /// Height taken by all outputs. - pub(crate) fn h_outputs (&self) -> u16 { - self.outputs_with_sizes() - .last() - .map(|(_, _, _, _, y)|y as u16).unwrap_or(0) - } - /// Height taken by visible device slots. - pub(crate) fn h_devices (&self) -> u16 { - 2 - //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) - } -} - pub(crate) struct ArrangerView<'a> { pub arrangement: &'a Arrangement, @@ -107,17 +34,18 @@ impl<'a> ArrangerView<'a> { arrangement: &'a Arrangement, editor: Option<&'a MidiEditor> ) -> Self { + let is_editing = editor.is_some(); let selected = arrangement.selected; let h_tracks_area = arrangement.h_tracks_area(); let h_scenes_area = arrangement.h_scenes_area(); - let h_scenes = arrangement.h_scenes(); + let h_scenes = arrangement.h_scenes(is_editing); Self { arrangement, - is_editing: editor.is_some(), + is_editing, width: arrangement.w(), - width_mid: arrangement.w_tracks_area(), - width_side: arrangement.w_sidebar(), + width_mid: arrangement.w_tracks_area(is_editing), + width_side: arrangement.w_sidebar(is_editing), inputs_height: arrangement.h_inputs(), inputs_count: arrangement.midi_ins.len(), @@ -214,4 +142,144 @@ impl<'a> ArrangerView<'a> { track.devices.get(0).map(|device|wrap(bg.rgb, fg, device.name())) })) } + + /// Default scene height. + pub(crate) const H_SCENE: usize = 2; + + /// Default editor height. + pub(crate) const H_EDITOR: usize = 15; + + /// Render scenes with clips + pub(crate) fn scenes (&'a self, editor: Option) -> impl Content + 'a { + /// A scene with size and color. + type SceneWithColor<'a> = (usize, &'a Scene, usize, usize, Option); + let Self { + arrangement, + width, width_side, width_mid, + scenes_height, scene_last, scene_selected, + track_selected, is_editing, .. + } = self; + + let selection = Has::>::get(self.arrangement); + let selected_track = selection.map(|s|s.track()).flatten(); + let selected_scene = selection.map(|s|s.scene()).flatten(); + + let scenes_with_scene_colors = ||HasScenes::scenes_with_sizes(self.arrangement, + *is_editing, + Self::H_SCENE, + Self::H_EDITOR, + selected_track, + selected_scene, + ).map_while(|(s, scene, y1, y2)|if y2 as u16 > *scenes_height { + None + } else { + Some((s, scene, y1, y2, if s == 0 { + None + } else { + Some(arrangement.scenes()[s-1].color) + })) + }); + + let scene_header = |(s, scene, y1, y2, previous): SceneWithColor, _|{ + let height = (1 + y2 - y1) as u16; + let name = Some(scene.name.clone()); + let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⯈ ", name)))); + let same_track = true; + let selected = same_track && *scene_selected == Some(s); + let neighbor = same_track && s > 0 && *scene_selected == Some(s - 1); + let is_last = *scene_last == s; + let theme = scene.color; + let fg = theme.lightest.rgb; + let bg = if selected { theme.light } else { theme.base }.rgb; + let hi = if let Some(previous) = previous { + if neighbor { + previous.light.rgb + } else { + previous.base.rgb + } + } else { + Reset + }; + let lo = if is_last { + Reset + } else if selected { + theme.light.rgb + } else { + theme.base.rgb + }; + Fill::x(map_south(y1 as u16, height, Fixed::y(height, Phat { + width: 0, height: 0, content, colors: [fg, bg, hi, lo] + }))) + }; + + let scenes_with_track_colors = |track: usize| arrangement.scenes_with_sizes( + self.is_editing, + Self::H_SCENE, + Self::H_EDITOR, + selected_track, + selected_scene, + ).map_while(|(s, scene, y1, y2)|if y2 as u16 > self.scenes_height { + None + } else { + Some((s, scene, y1, y2, if s == 0 { + None + } else { + Some(self.arrangement.scenes[s-1].clips[track].as_ref() + .map(|c|c.read().unwrap().color) + .unwrap_or(ItemTheme::G[32])) + })) + }); + + Tryptich::center(*scenes_height) + + .left(*width_side, Map::new( + ||scenes_with_scene_colors(), + scene_header)) + + .middle(*width_mid, per_track( + *width_mid, + ||self.tracks_with_sizes_scrolled(), + |track_index, track|Map::new( + ||scenes_with_track_colors(track_index), + |(s, scene, y1, y2, previous): SceneWithColor<'a>, _|{ + let (name, theme) = if let Some(clip) = &scene.clips[track_index] { + let clip = clip.read().unwrap(); + (Some(clip.name.clone()), clip.color) + } else { + (None, ItemTheme::G[32]) + }; + let height = (1 + y2 - y1) as u16; + let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⏹ ", name)))); + let same_track = *track_selected == Some(track_index); + let selected = same_track && *scene_selected == Some(s); + let neighbor = same_track && s > 0 && *scene_selected == Some(s - 1); + let is_last = *scene_last == s; + let fg = theme.lightest.rgb; + let bg = if selected { theme.light } else { theme.base }.rgb; + let hi = if let Some(previous) = previous { + if neighbor { + previous.light.rgb + } else { + previous.base.rgb + } + } else { + Reset + }; + let lo = if is_last { + Reset + } else if selected { + theme.light.rgb + } else { + theme.base.rgb + }; + map_south(y1 as u16, height, Bsp::b(Fixed::y(height, Phat { + width: 0, height: 0, content, colors: [fg, bg, hi, lo] + }), When( + *is_editing && same_track && *scene_selected == Some(s), + editor + ))) + }))) + + } + } diff --git a/crates/device/src/clock.rs b/crates/device/src/clock.rs index 31b19c44..f16baafe 100644 --- a/crates/device/src/clock.rs +++ b/crates/device/src/clock.rs @@ -1,3 +1,5 @@ +use crate::*; + mod clock_api; pub use self::clock_api::*; mod clock_model; pub use self::clock_model::*; mod clock_view; pub use self::clock_view::*; @@ -7,11 +9,11 @@ pub trait HasClock: Send + Sync { fn clock_mut (&mut self) -> &mut Clock; } -#[macro_export] macro_rules! has_clock { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasClock for $Struct $(<$($L),*$($T),*>)? { - fn clock (&$self) -> &Clock { &$cb } - fn clock_mut (&mut $self) -> &mut Clock { &mut $cb } - } +impl> HasClock for T { + fn clock (&self) -> &Clock { + self.get() + } + fn clock_mut (&mut self) -> &mut Clock { + self.get_mut() } } diff --git a/crates/device/src/device.rs b/crates/device/src/device.rs index 70da6c28..91e45fa0 100644 --- a/crates/device/src/device.rs +++ b/crates/device/src/device.rs @@ -1,5 +1,19 @@ use crate::*; +impl> + Has> HasDevices for T { + fn devices (&self) -> &Vec { + self.get() + } + fn devices_mut (&mut self) -> &mut Vec { + self.get_mut() + } +} + +pub trait HasDevices: Has { + fn devices (&self) -> &Vec; + fn devices_mut (&mut self) -> &mut Vec; +} + #[derive(Debug)] pub enum Device { #[cfg(feature = "sampler")] @@ -49,3 +63,7 @@ audio!(|self: DeviceAudio<'a>, client, scope|{ Sf2 => { todo!() }, // TODO } }); + +#[tengri_proc::command(Device)] +impl DeviceCommand { +} diff --git a/crates/device/src/pool/pool_model.rs b/crates/device/src/pool/pool_model.rs index 8347ddb7..f6034bf3 100644 --- a/crates/device/src/pool/pool_model.rs +++ b/crates/device/src/pool/pool_model.rs @@ -4,11 +4,11 @@ use crate::*; pub struct Pool { pub visible: bool, /// Collection of clips - pub clips: Arc>>>>, + pub clips: Arc>>>>, /// Selected clip - pub clip: AtomicUsize, + pub clip: AtomicUsize, /// Mode switch - pub mode: Option, + pub mode: Option, /// Embedded file browser pub browser: Option, } diff --git a/crates/device/src/sequencer/seq_model.rs b/crates/device/src/sequencer/seq_model.rs index 2282edb1..a4dd982d 100644 --- a/crates/device/src/sequencer/seq_model.rs +++ b/crates/device/src/sequencer/seq_model.rs @@ -1,20 +1,20 @@ //! MIDI sequencer use crate::*; +impl> HasSequencer for T { + fn sequencer (&self) -> &impl MidiSequencer { + self.get() + } + fn sequencer_mut (&mut self) -> &mut impl MidiSequencer { + self.get_mut() + } +} + pub trait HasSequencer { fn sequencer (&self) -> &impl MidiSequencer; fn sequencer_mut (&mut self) -> &mut impl MidiSequencer; } -#[macro_export] macro_rules! has_sequencer { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasSequencer for $Struct $(<$($L),*$($T),*>)? { - fn sequencer (&$self) -> &impl MidiSequencer { &$cb } - fn sequencer_mut (&mut $self) -> &mut impl MidiSequencer { &mut$cb } - } - } -} - /// Contains state for playing a clip pub struct Sequencer { /// State of clock and playhead @@ -102,7 +102,7 @@ impl std::fmt::Debug for Sequencer { } } -has_clock!(|self: Sequencer|self.clock); +has!(Clock: |self: Sequencer|self.clock); has!(Vec: |self:Sequencer| self.midi_ins); has!(Vec: |self:Sequencer| self.midi_outs); diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index d097e945..d63ae531 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -5,6 +5,7 @@ version = { workspace = true } [dependencies] tengri = { workspace = true } +tengri_proc = { workspace = true } jack = { workspace = true } midly = { workspace = true } uuid = { workspace = true } diff --git a/crates/engine/src/jack/jack_client.rs b/crates/engine/src/jack/jack_client.rs index b1200529..d8d6be6e 100644 --- a/crates/engine/src/jack/jack_client.rs +++ b/crates/engine/src/jack/jack_client.rs @@ -2,6 +2,12 @@ use crate::*; use super::*; use self::JackState::*; +impl> HasJack for T { + fn jack (&self) -> &Jack { + self.get() + } +} + /// Things that can provide a [jack::Client] reference. pub trait HasJack { /// Return the internal [jack::Client] handle diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 67482aa5..e9bcf677 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -9,7 +9,7 @@ pub(crate) use std::sync::{Arc, atomic::{AtomicUsize, AtomicBool, Ordering::Rela pub(crate) use std::fmt::Debug; pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem}; -pub(crate) use ::tengri::{from, Usually, tui::*}; +pub(crate) use ::tengri::{from, Usually, Perhaps, tui::*}; pub use ::atomic_float; pub(crate) use atomic_float::*; diff --git a/crates/engine/src/midi/midi_in.rs b/crates/engine/src/midi/midi_in.rs index 800bf2bc..27cbd76e 100644 --- a/crates/engine/src/midi/midi_in.rs +++ b/crates/engine/src/midi/midi_in.rs @@ -1,5 +1,9 @@ use crate::*; +#[tengri_proc::command(JackMidiIn)] +impl MidiInputCommand { +} + impl>> HasMidiIns for T { fn midi_ins (&self) -> &Vec { self.get() @@ -32,11 +36,20 @@ pub trait HasMidiIns { data }) } - fn midi_in_add (&mut self, jack: &Jack) -> Usually<()> { +} + +pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; + +impl AddMidiIn for T { + fn midi_in_add (&mut self) -> Usually<()> { let index = self.midi_ins().len(); - self.midi_ins_mut().push(JackMidiIn::new(jack, &format!("M/{index}"), &[])?); + let port = JackMidiIn::new(self.jack(), &format!("M/{index}"), &[])?; + self.midi_ins_mut().push(port); Ok(()) } } -pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; +/// May create new MIDI input ports. +pub trait AddMidiIn { + fn midi_in_add (&mut self) -> Usually<()>; +} diff --git a/crates/engine/src/midi/midi_out.rs b/crates/engine/src/midi/midi_out.rs index 1bc3a74d..89a22296 100644 --- a/crates/engine/src/midi/midi_out.rs +++ b/crates/engine/src/midi/midi_out.rs @@ -1,5 +1,9 @@ use crate::*; +#[tengri_proc::command(JackMidiOut)] +impl MidiOutputCommand { +} + impl>> HasMidiOuts for T { fn midi_outs (&self) -> &Vec { self.get() @@ -25,9 +29,19 @@ pub trait HasMidiOuts { data }) } - fn midi_out_add (&mut self, jack: &Jack) -> Usually<()> { +} + +/// Trail for thing that may gain new MIDI ports. +impl AddMidiOut for T { + fn midi_out_add (&mut self) -> Usually<()> { let index = self.midi_outs().len(); - self.midi_outs_mut().push(JackMidiOut::new(&jack, &format!("{index}/M"), &[])?); + let port = JackMidiOut::new(self.jack(), &format!("{index}/M"), &[])?; + self.midi_outs_mut().push(port); Ok(()) } } + +/// May create new MIDI output ports. +pub trait AddMidiOut { + fn midi_out_add (&mut self) -> Usually<()>; +} From ebdb8881e9c7679b015e8d1def081decf65c0b2f Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 14 May 2025 14:35:19 +0300 Subject: [PATCH 03/12] wip: down to 13 errors --- crates/app/src/model.rs | 138 ++++-- crates/device/src/arranger/arranger_api.rs | 110 +---- crates/device/src/arranger/arranger_clip.rs | 94 ++-- crates/device/src/arranger/arranger_model.rs | 24 +- crates/device/src/arranger/arranger_scene.rs | 36 +- crates/device/src/arranger/arranger_select.rs | 8 +- crates/device/src/arranger/arranger_track.rs | 236 +++------- crates/device/src/arranger/arranger_tracks.rs | 145 +++++++ crates/device/src/arranger/arranger_view.rs | 26 ++ crates/device/src/clock/clock_model.rs | 21 + crates/device/src/sequencer/seq_audio.rs | 405 +++++++----------- crates/device/src/sequencer/seq_model.rs | 44 +- crates/engine/src/jack/jack_port.rs | 4 +- crates/engine/src/lib.rs | 10 + crates/engine/src/midi.rs | 14 +- crates/engine/src/midi/midi_hold.rs | 10 + crates/engine/src/midi/midi_in.rs | 6 + crates/engine/src/midi/midi_out.rs | 151 +++++++ deps/tengri | 2 +- 19 files changed, 793 insertions(+), 691 deletions(-) create mode 100644 crates/engine/src/midi/midi_hold.rs diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index 18c6d61f..ad036139 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -6,27 +6,21 @@ pub struct App { pub jack: Jack, /// Port handles pub ports: std::collections::BTreeMap>, - /// Source of time - pub clock: Clock, /// Display size pub size: Measure, /// Performance counter pub perf: PerfModel, // View and input definition - pub config: Configuration, - /// Undo history - pub history: Vec, - // Dialog overlay - pub dialog: Option, - /// Browses external resources, such as directories - pub browser: Option, - /// Contains all clips in the project - pub pool: Option, - /// Contains the currently edited MIDI clip - pub editor: Option, + pub config: Configuration, /// Contains the currently edited musical arrangement - pub arranger: Arrangement, + pub project: Arrangement, + /// Undo history + pub history: Vec, + // Dialog overlay + pub dialog: Option, + /// Contains the currently edited MIDI clip + pub editor: Option, // Cache of formatted strings pub view_cache: Arc>, @@ -34,11 +28,12 @@ pub struct App { pub color: ItemTheme, } -has!(Option: |self: App|self.arrangement.selected); -has!(Vec: |self: App|self.arrangement.midi_ins); -has!(Vec: |self: App|self.arrangement.midi_outs); -has!(Vec: |self: App|self.arrangement.scenes); -has!(Vec: |self: App|self.arrangement.tracks); +has!(Option: |self: App|self.project.selected); +has!(Vec: |self: App|self.project.midi_ins); +has!(Vec: |self: App|self.project.midi_outs); +has!(Vec: |self: App|self.project.scenes); +has!(Vec: |self: App|self.project.tracks); +has!(Clock: |self: App|self.project.clock); impl App { pub fn toggle_dialog (&mut self, dialog: Option) { @@ -67,11 +62,85 @@ impl App { } pub(crate) fn device_add (&mut self, index: usize) -> Usually<()> { match index { - 0 => self.arrangement.device_add_sampler(), - 1 => self.arrangement.device_add_lv2(), + 0 => self.device_add_sampler(), + 1 => self.device_add_lv2(), _ => unreachable!(), } } + fn device_add_lv2 (&mut self) -> Usually<()> { + todo!(); + Ok(()) + } + fn device_add_sampler (&mut self, jack: &Jack) -> Usually { + let name = jack.with_client(|c|c.name().to_string()); + let midi = self.track().expect("no active track").sequencer.midi_outs[0].name(); + let sampler = if let Ok(sampler) = Sampler::new( + jack, + &format!("{}/Sampler", Has::::get(self).name), + &[PortConnect::exact(format!("{name}:{midi}"))], + &[&[], &[]], + &[&[], &[]] + ) { + self.dialog = None; + Device::Sampler(sampler) + } else { + self.dialog = Some(Dialog::Message(Message::FailedToAddDevice)); + return Err("failed to add device".into()) + }; + Has::::get_mut(self).expect("no active track").devices.push(sampler); + Ok(()) + } + // Create new clip in pool when entering empty cell + fn clip_auto_create (&mut self) -> Option>> { + if let Some(pool) = Has::>::get(self) + && let Some(Selection::TrackClip { track, scene }) = self.get() + && let Some(scene) = Has::>::get_mut(self).get_mut(*scene) + && let Some(slot) = scene.clips.get_mut(*track) + && slot.is_none() + && let Some(track) = Has::>::get_mut(self).get_mut(*track) + { + let (index, mut clip) = pool.add_new_clip(); + // autocolor: new clip colors from scene and track color + let color = track.color.base.mix(scene.color.base, 0.5); + clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); + if let Some(ref mut editor) = Has::>::get_mut(self) { + editor.set_clip(Some(&clip)); + } + *slot = Some(clip.clone()); + Some(clip) + } else { + None + } + } +} + +#[cfg(all(feature = "pool", feature = "editor"))] +pub trait AutoRemove: + Has> + + Has> + + Has> + + Has> + + Has> + + Send + Sync +{ + // Remove clip from arrangement when exiting empty clip editor + fn clip_auto_remove (&mut self) { + if let Some(ref mut pool) = Has::>::get(self) + && let Some(Selection::TrackClip { track, scene }) = self.get() + && let Some(scene) = Has::>::get_mut(self).get_mut(*scene) + && let Some(slot) = scene.clips.get_mut(*track) + && let Some(clip) = slot.as_mut() + { + let mut swapped = None; + if clip.read().unwrap().count_midi_messages() == 0 { + std::mem::swap(&mut swapped, slot); + } + if let Some(clip) = swapped { + pool.delete_clip(&clip.read().unwrap()); + } + } + } +} } /// Various possible dialog overlays @@ -81,8 +150,8 @@ pub enum Dialog { Menu, Device(usize), Message(Message), - Save, - Load, + Save(Browser), + Load(Browser), Options, } @@ -234,27 +303,4 @@ impl App { 0 } } - fn device_add_lv2 (&mut self) -> Usually<()> { - todo!(); - Ok(()) - } - fn device_add_sampler (&mut self, jack: &Jack) -> Usually { - let name = jack.with_client(|c|c.name().to_string()); - let midi = self.track().expect("no active track").sequencer.midi_outs[0].name(); - let sampler = if let Ok(sampler) = Sampler::new( - jack, - &format!("{}/Sampler", Has::::get(self).name), - &[PortConnect::exact(format!("{name}:{midi}"))], - &[&[], &[]], - &[&[], &[]] - ) { - self.dialog = None; - Device::Sampler(sampler) - } else { - self.dialog = Some(Dialog::Message(Message::FailedToAddDevice)); - return Err("failed to add device".into()) - }; - Has::::get_mut(self).expect("no active track").devices.push(sampler); - Ok(()) - } } diff --git a/crates/device/src/arranger/arranger_api.rs b/crates/device/src/arranger/arranger_api.rs index bd6d4721..9034baca 100644 --- a/crates/device/src/arranger/arranger_api.rs +++ b/crates/device/src/arranger/arranger_api.rs @@ -6,6 +6,7 @@ impl Arrangement { fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } + fn _todo_opt_selection_ (&self) -> Option { todo!() } } #[tengri_proc::command(Arrangement)] @@ -200,117 +201,20 @@ impl ArrangementCommand { } } -impl<'state> Context<'state, TrackCommand> for Arrangement { - fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { +impl<'state> Context<'state, MidiInputCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { Context::get(&self, iter) } } -#[tengri_proc::expose] -impl Track { - 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!() } -} - -#[tengri_proc::command(Track)] -impl TrackCommand { - fn set_name (track: &mut Track, mut name: Arc) -> Perhaps { - std::mem::swap(&mut name, &mut track.name); - Ok(Some(Self::SetName { name })) - } - fn set_color (track: &mut Track, mut color: ItemTheme) -> Perhaps { - std::mem::swap(&mut color, &mut track.color); - Ok(Some(Self::SetColor { color })) - } - fn set_mute (track: &mut Track, value: Option) -> Perhaps { - todo!() - } - fn set_solo (track: &mut Track, value: Option) -> Perhaps { - todo!() - } - fn set_rec (track: &mut Track, value: Option) -> Perhaps { - let current = track.sequencer.recording; - let value = value.unwrap_or(!current); - Ok((value != current).then_some(Self::SetRec { value: Some(current) })) - } - fn set_mon (track: &mut Track, value: Option) -> Perhaps { - let current = track.sequencer.monitoring; - let value = value.unwrap_or(!current); - Ok((value != current).then_some(Self::SetRec { value: Some(current) })) - } - fn set_size (track: &mut Track, size: usize) -> Perhaps { - todo!() - } - fn set_zoom (track: &mut Track, zoom: usize) -> Perhaps { - todo!() - } - fn stop (track: &mut Track) -> Perhaps { - track.sequencer.enqueue_next(None); - Ok(None) - } -} - -impl<'state> Context<'state, SceneCommand> for Arrangement { - fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { +impl<'state> Context<'state, MidiOutputCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { Context::get(&self, iter) } } -impl<'state> Context<'state, ClipCommand> for Arrangement { - fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { +impl<'state> Context<'state, DeviceCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { Context::get(&self, iter) } } - -#[tengri_proc::expose] -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!() } -} - -#[tengri_proc::command(Scene)] -impl SceneCommand { - fn set_name (scene: &mut Scene, mut name: Arc) -> Perhaps { - std::mem::swap(&mut scene.name, &mut name); - Ok(Some(Self::SetName { name })) - } - fn set_color (scene: &mut Scene, mut color: ItemTheme) -> Perhaps { - std::mem::swap(&mut scene.color, &mut color); - Ok(Some(Self::SetColor { color })) - } - fn set_size (scene: &mut Scene, size: usize) -> Perhaps { - todo!() - } - fn set_zoom (scene: &mut Scene, zoom: usize) -> Perhaps { - todo!() - } -} - -#[tengri_proc::expose] -impl MidiClip { - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_bool_stub_ (&self) -> bool { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } - fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } -} - -#[tengri_proc::command(MidiClip)] -impl ClipCommand { - fn set_color (clip: &mut MidiClip, color: Option) -> Perhaps { - //(SetColor [t: usize, s: usize, c: ItemTheme] - //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); - //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) - todo!() - } - fn set_loop (clip: &mut MidiClip, looping: Option) -> Perhaps { - //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) - //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) - todo!() - } -} diff --git a/crates/device/src/arranger/arranger_clip.rs b/crates/device/src/arranger/arranger_clip.rs index b5acd217..b0ad719a 100644 --- a/crates/device/src/arranger/arranger_clip.rs +++ b/crates/device/src/arranger/arranger_clip.rs @@ -1,5 +1,36 @@ use crate::*; +impl<'state> Context<'state, ClipCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { + Context::get(&self, iter) + } +} + +#[tengri_proc::expose] +impl MidiClip { + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_bool_stub_ (&self) -> bool { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } +} + +#[tengri_proc::command(MidiClip)] +impl ClipCommand { + fn set_color (clip: &mut MidiClip, color: Option) -> Perhaps { + //(SetColor [t: usize, s: usize, c: ItemTheme] + //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); + //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) + todo!() + } + fn set_loop (clip: &mut MidiClip, looping: Option) -> Perhaps { + //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) + //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) + todo!() + } +} + impl Arrangement { /// Put a clip in a slot @@ -26,7 +57,7 @@ impl Arrangement { /// Get the active clip pub(crate) fn clip (&self) -> Option>> { - self.scene()?.clips.get(self.selected().track()?)?.clone() + self.scene()?.clips.get(self.selected.track()?)?.clone() } /// Toggle looping for the active clip @@ -37,64 +68,3 @@ impl Arrangement { } } - -#[cfg(all(feature = "pool", feature = "editor"))] -pub trait AutoCreate: - Has> + - Has> + - Has> + - Has> + - Has> + - Send + Sync -{ - // Create new clip in pool when entering empty cell - fn clip_auto_create (&mut self) -> Option>> { - if let Some(pool) = Has::>::get(self) - && let Some(Selection::TrackClip { track, scene }) = self.get() - && let Some(scene) = Has::>::get_mut(self).get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && slot.is_none() - && let Some(track) = Has::>::get_mut(self).get_mut(track) - { - let (index, mut clip) = pool.add_new_clip(); - // autocolor: new clip colors from scene and track color - let color = track.color.base.mix(scene.color.base, 0.5); - clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); - if let Some(ref mut editor) = Has::>::get_mut(self) { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip.clone()); - Some(clip) - } else { - None - } - } -} - -#[cfg(all(feature = "pool", feature = "editor"))] -pub trait AutoRemove: - Has> + - Has> + - Has> + - Has> + - Has> + - Send + Sync -{ - // Remove clip from arrangement when exiting empty clip editor - fn clip_auto_remove (&mut self) { - if let Some(ref mut pool) = Has::>::get(self) - && let Some(Selection::TrackClip { track, scene }) = self.get() - && let Some(scene) = Has::>::get_mut(self).get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && let Some(clip) = slot.as_mut() - { - let mut swapped = None; - if clip.read().unwrap().count_midi_messages() == 0 { - std::mem::swap(&mut swapped, slot); - } - if let Some(clip) = swapped { - pool.delete_clip(&clip.read().unwrap()); - } - } - } -} diff --git a/crates/device/src/arranger/arranger_model.rs b/crates/device/src/arranger/arranger_model.rs index e12e73d1..d2f3246c 100644 --- a/crates/device/src/arranger/arranger_model.rs +++ b/crates/device/src/arranger/arranger_model.rs @@ -6,6 +6,10 @@ pub struct Arrangement { pub name: Arc, /// Base color. pub color: ItemTheme, + /// Jack client handle + pub jack: Jack, + /// Source of time + pub clock: Clock, /// List of global midi inputs pub midi_ins: Vec, /// List of global midi outputs @@ -35,8 +39,8 @@ pub struct Arrangement { pub arranger: Arc>, /// Display size pub size: Measure, - /// Jack client handle - pub jack: Jack, + /// Contains all clips in arrangement + pub pool: Pool, } has!(Option: |self: Arrangement|self.selected); @@ -45,6 +49,7 @@ has!(Vec: |self: Arrangement|self.midi_outs); has!(Vec: |self: Arrangement|self.scenes); has!(Vec: |self: Arrangement|self.tracks); has!(Jack: |self: Arrangement|self.jack); +has!(Clock: |self: Arrangement|self.clock); impl Arrangement { /// Width of display @@ -57,7 +62,8 @@ impl Arrangement { } /// Width taken by all tracks. pub(crate) fn w_tracks (&self) -> u16 { - self.tracks_with_sizes().last().map(|(_, _, _, x)|x as u16).unwrap_or(0) + self.tracks_with_sizes(&self.selected, None).last() + .map(|(_, _, _, x)|x as u16).unwrap_or(0) } /// Width available to display tracks. pub(crate) fn w_tracks_area (&self, is_editing: bool) -> u16 { @@ -114,3 +120,15 @@ impl Arrangement { //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } } + +#[cfg(feature = "sampler")] +impl Arrangement { + /// Get the first sampler of the active track + pub fn sampler (&self) -> Option<&Sampler> { + self.track().map(|t|t.sampler(0)).flatten() + } + /// Get the first sampler of the active track + pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { + self.track_mut().map(|t|t.sampler_mut(0)).flatten() + } +} diff --git a/crates/device/src/arranger/arranger_scene.rs b/crates/device/src/arranger/arranger_scene.rs index a217d43e..209d5397 100644 --- a/crates/device/src/arranger/arranger_scene.rs +++ b/crates/device/src/arranger/arranger_scene.rs @@ -1,10 +1,42 @@ use crate::*; +impl<'state> Context<'state, SceneCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { + Context::get(&self, iter) + } +} + +#[tengri_proc::expose] +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!() } +} + +#[tengri_proc::command(Scene)] +impl SceneCommand { + fn set_name (scene: &mut Scene, mut name: Arc) -> Perhaps { + std::mem::swap(&mut scene.name, &mut name); + Ok(Some(Self::SetName { name })) + } + fn set_color (scene: &mut Scene, mut color: ItemTheme) -> Perhaps { + std::mem::swap(&mut scene.color, &mut color); + Ok(Some(Self::SetColor { color })) + } + fn set_size (scene: &mut Scene, size: usize) -> Perhaps { + todo!() + } + fn set_zoom (scene: &mut Scene, zoom: usize) -> Perhaps { + todo!() + } +} + impl> + Send + Sync> HasScene for T {} pub trait HasScene: Has> + Send + Sync { - fn scene (&self) -> &Option { - Has::>::get(self) + fn scene (&self) -> Option<&Scene> { + Has::>::get(self).as_ref() } fn scene_mut (&mut self) -> &mut Option { Has::>::get_mut(self) diff --git a/crates/device/src/arranger/arranger_select.rs b/crates/device/src/arranger/arranger_select.rs index 2ef03033..8330faf2 100644 --- a/crates/device/src/arranger/arranger_select.rs +++ b/crates/device/src/arranger/arranger_select.rs @@ -13,9 +13,9 @@ pub trait HasSelection: Has { impl Has> for Arrangement { fn get (&self) -> &Option { - Has::>::get(self) - .and_then(|selection|selection.track()) - .and_then(|index|Has::>::get(self).get(index)) + let selection: Selection = Has::>::get(self)?; + let index: Option = selection.track()?; + let track: Track = Has::>::get(self).get(index)?; } fn get_mut (&mut self) -> &mut Option { Has::>::get(self) @@ -33,7 +33,7 @@ impl Has> for Arrangement { fn get_mut (&mut self) -> &mut Option { Has::>::get(self) .and_then(|selection|selection.track()) - .and_then(|index|Has::>::get_mut(self).get_mut(index)) + .and_then(|index|Has::>::get_mut(self).get_mut(index).as_deref()) } } diff --git a/crates/device/src/arranger/arranger_track.rs b/crates/device/src/arranger/arranger_track.rs index 41cb5bf1..26449380 100644 --- a/crates/device/src/arranger/arranger_track.rs +++ b/crates/device/src/arranger/arranger_track.rs @@ -1,5 +1,57 @@ use crate::*; +impl<'state> Context<'state, TrackCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { + Context::get(&self, iter) + } +} + +#[tengri_proc::expose] +impl Track { + 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!() } +} + +#[tengri_proc::command(Track)] +impl TrackCommand { + fn set_name (track: &mut Track, mut name: Arc) -> Perhaps { + std::mem::swap(&mut name, &mut track.name); + Ok(Some(Self::SetName { name })) + } + fn set_color (track: &mut Track, mut color: ItemTheme) -> Perhaps { + std::mem::swap(&mut color, &mut track.color); + Ok(Some(Self::SetColor { color })) + } + fn set_mute (track: &mut Track, value: Option) -> Perhaps { + todo!() + } + fn set_solo (track: &mut Track, value: Option) -> Perhaps { + todo!() + } + fn set_rec (track: &mut Track, value: Option) -> Perhaps { + let current = track.sequencer.recording; + let value = value.unwrap_or(!current); + Ok((value != current).then_some(Self::SetRec { value: Some(current) })) + } + fn set_mon (track: &mut Track, value: Option) -> Perhaps { + let current = track.sequencer.monitoring; + let value = value.unwrap_or(!current); + Ok((value != current).then_some(Self::SetRec { value: Some(current) })) + } + fn set_size (track: &mut Track, size: usize) -> Perhaps { + todo!() + } + fn set_zoom (track: &mut Track, zoom: usize) -> Perhaps { + todo!() + } + fn stop (track: &mut Track) -> Perhaps { + track.sequencer.enqueue_next(None); + Ok(None) + } +} + #[derive(Debug, Default)] pub struct Track { /// Name of track @@ -97,77 +149,6 @@ impl Track { } } -impl> + Send + Sync> HasTracks for T {} - -pub trait HasTracks: Has> + Send + Sync { - fn tracks (&self) -> &Vec { - Has::>::get(self) - } - fn tracks_mut (&mut self) -> &mut Vec { - Has::>::get_mut(self) - } - /// Run audio callbacks for every track and every device - fn tracks_render (&mut self, client: &Client, scope: &ProcessScope) -> Control { - for track in self.tracks_mut().iter_mut() { - if Control::Quit == PlayerAudio( - track.sequencer_mut(), &mut self.note_buf, &mut self.midi_buf - ).process(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); - } - } else { - for track in self.tracks_mut().iter_mut() { - track.sequencer.enqueue_next(None); - } - } - } - /// Iterate over tracks with their corresponding sizes. - fn tracks_with_sizes ( - &self, - selection: &Option, - editor_width: Option - ) -> impl TracksSizes<'_> { - let mut x = 0; - let active_track = if let Some(width) = editor_width && let Some(selection) = selection { - selection.track() - } else { - None - }; - self.tracks().iter().enumerate().map(move |(index, track)|{ - let width = active_track - .and_then(|_|editor_width) - .unwrap_or(track.width.max(8)); - let data = (index, track, x, x + width); - x += width + Self::TRACK_SPACING; - data - }) - } - /// Spacing between tracks. - const TRACK_SPACING: usize = 0; -} - impl> + Send + Sync> HasTrack for T {} pub trait HasTrack: Has> + Send + Sync { @@ -178,116 +159,3 @@ pub trait HasTrack: Has> + Send + Sync { Has::>::get_mut(self).as_mut() } } - -impl<'a> ArrangerView<'a> { - pub(crate) fn tracks_with_sizes_scrolled (&'a self) - -> impl TracksSizes<'a> - { - self.arrangement - .tracks_with_sizes(&self.arrangement.selected, self.is_editing.then_some(20/*FIXME*/)) - .map_while(move|(t, track, x1, x2)|{ - (self.width_mid > x2 as u16).then_some((t, track, x1, x2)) - }) - } -} - -pub(crate) fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( - width: u16, - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Content + 'a { - Align::x(Tui::bg(Reset, Map::new(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - map_east(x1 as u16, width, Fixed::x(width, Tui::fg_bg( - track.color.lightest.rgb, - track.color.base.rgb, - callback(index, track))))}))) -} - -pub(crate) fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( - width: u16, - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Content + 'a { - per_track_top( - width, - tracks, - move|index, track|Fill::y(Align::y(callback(index, track))) - ) -} - -impl AddTrack for T {} - -pub trait AddTrack: HasTracks + HasScenes + HasClock + HasJack { - /// Add multiple tracks - fn tracks_add ( - &mut self, - count: usize, - width: Option, - mins: &[PortConnect], - mouts: &[PortConnect], - ) -> Usually<()> { - let jack = self.jack().clone(); - let track_color_1 = ItemColor::random(); - let track_color_2 = ItemColor::random(); - for i in 0..count { - let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); - let mut track = self.track_add(None, Some(color), mins, mouts)?.1; - if let Some(width) = width { - track.width = width; - } - } - Ok(()) - } - - /// Add a track - fn track_add ( - &mut self, - name: Option<&str>, - color: Option, - mins: &[PortConnect], - mouts: &[PortConnect], - ) -> Usually<(usize, &mut Track)> { - self.track_last += 1; - let name: Arc = name.map_or_else( - ||format!("Track{:02}", self.track_last).into(), - |x|x.to_string().into() - ); - let mut track = Track { - width: (name.len() + 2).max(12), - color: color.unwrap_or_else(ItemTheme::random), - sequencer: Sequencer::new( - &format!("{name}"), - self.jack(), - Some(self.clock()), - None, - mins, - mouts - )?, - name, - ..Default::default() - }; - self.tracks_mut().push(track); - let len = self.tracks().len(); - let index = len - 1; - for scene in self.scenes_mut().iter_mut() { - while scene.clips.len() < len { - scene.clips.push(None); - } - } - Ok((index, &mut self.tracks_mut()[index])) - } -} - -#[cfg(feature = "sampler")] -impl Arrangement { - /// Get the first sampler of the active track - pub fn sampler (&self) -> Option<&Sampler> { - self.track().map(|t|t.sampler(0)).flatten() - } - /// Get the first sampler of the active track - pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.track_mut().map(|t|t.sampler_mut(0)).flatten() - } -} diff --git a/crates/device/src/arranger/arranger_tracks.rs b/crates/device/src/arranger/arranger_tracks.rs index e69de29b..7863a9e7 100644 --- a/crates/device/src/arranger/arranger_tracks.rs +++ b/crates/device/src/arranger/arranger_tracks.rs @@ -0,0 +1,145 @@ +use crate::*; + +impl> + Send + Sync> HasTracks for T {} + +pub trait HasTracks: Has> + Send + Sync { + fn tracks (&self) -> &Vec { + Has::>::get(self) + } + fn tracks_mut (&mut self) -> &mut Vec { + Has::>::get_mut(self) + } + /// 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); + } + } + } + /// Iterate over tracks with their corresponding sizes. + fn tracks_with_sizes ( + &self, + selection: &Option, + editor_width: Option + ) -> impl TracksSizes<'_> { + let mut x = 0; + let active_track = if let Some(width) = editor_width && let Some(selection) = selection { + selection.track() + } else { + None + }; + self.tracks().iter().enumerate().map(move |(index, track)|{ + let width = active_track + .and_then(|_|editor_width) + .unwrap_or(track.width.max(8)); + let data = (index, track, x, x + width); + x += width + Self::TRACK_SPACING; + data + }) + } + /// Spacing between tracks. + const TRACK_SPACING: usize = 0; +} + +impl<'a> ArrangerView<'a> { + pub(crate) fn tracks_with_sizes_scrolled (&'a self) + -> impl TracksSizes<'a> + { + self.arrangement + .tracks_with_sizes(&self.arrangement.selected, self.is_editing.then_some(20/*FIXME*/)) + .map_while(move|(t, track, x1, x2)|{ + (self.width_mid > x2 as u16).then_some((t, track, x1, x2)) + }) + } +} + +impl AddTrack for T {} + +pub trait AddTrack: HasTracks + HasScenes + HasClock + HasJack { + /// Add multiple tracks + fn tracks_add ( + &mut self, + count: usize, + width: Option, + mins: &[PortConnect], + mouts: &[PortConnect], + ) -> Usually<()> { + let jack = self.jack().clone(); + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..count { + let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); + let mut track = self.track_add(None, Some(color), mins, mouts)?.1; + if let Some(width) = width { + track.width = width; + } + } + Ok(()) + } + + /// Add a track + fn track_add ( + &mut self, + name: Option<&str>, + color: Option, + mins: &[PortConnect], + mouts: &[PortConnect], + ) -> Usually<(usize, &mut Track)> { + self.track_last += 1; + let name: Arc = name.map_or_else( + ||format!("Track{:02}", self.track_last).into(), + |x|x.to_string().into() + ); + let mut track = Track { + width: (name.len() + 2).max(12), + color: color.unwrap_or_else(ItemTheme::random), + sequencer: Sequencer::new( + &format!("{name}"), + self.jack(), + Some(self.clock()), + None, + mins, + mouts + )?, + name, + ..Default::default() + }; + self.tracks_mut().push(track); + let len = self.tracks().len(); + let index = len - 1; + for scene in self.scenes_mut().iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); + } + } + Ok((index, &mut self.tracks_mut()[index])) + } +} diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs index e28d3362..e6213432 100644 --- a/crates/device/src/arranger/arranger_view.rs +++ b/crates/device/src/arranger/arranger_view.rs @@ -283,3 +283,29 @@ impl<'a> ArrangerView<'a> { } } + +pub(crate) fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( + width: u16, + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Content + 'a { + Align::x(Tui::bg(Reset, Map::new(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + let width = (x2 - x1) as u16; + map_east(x1 as u16, width, Fixed::x(width, Tui::fg_bg( + track.color.lightest.rgb, + track.color.base.rgb, + callback(index, track))))}))) +} + +pub(crate) fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( + width: u16, + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Content + 'a { + per_track_top( + width, + tracks, + move|index, track|Fill::y(Align::y(callback(index, track))) + ) +} diff --git a/crates/device/src/clock/clock_model.rs b/crates/device/src/clock/clock_model.rs index e9016264..c961233a 100644 --- a/crates/device/src/clock/clock_model.rs +++ b/crates/device/src/clock/clock_model.rs @@ -177,4 +177,25 @@ impl Clock { pub fn next_launch_instant (&self) -> Moment { Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) } + + /// Get index of first sample to populate. + /// + /// Greater than 0 means that the first pulse of the clip + /// falls somewhere in the middle of the chunk. + pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ + (scope.last_frame_time() as usize).saturating_sub( + started.sample.get() as usize + + self.started.read().unwrap().as_ref().unwrap().sample.get() as usize + ) + } + + // Get iterator that emits sample paired with pulse. + // + // * Sample: index into output buffer at which to write MIDI event + // * Pulse: index into clip from which to take the MIDI event + // + // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. + pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> TicksIterator { + self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) + } } diff --git a/crates/device/src/sequencer/seq_audio.rs b/crates/device/src/sequencer/seq_audio.rs index c96b7cdc..060d833b 100644 --- a/crates/device/src/sequencer/seq_audio.rs +++ b/crates/device/src/sequencer/seq_audio.rs @@ -1,82 +1,45 @@ use crate::*; -/// Hosts the JACK callback for a single MIDI sequencer -pub struct PlayerAudio<'a, T: MidiSequencer>( - /// Player - pub &'a mut T, - /// Note buffer - pub &'a mut Vec, - /// Note chunk buffer - pub &'a mut Vec>>, -); - /// JACK process callback for a sequencer's clip sequencer/recorder. -impl Audio for PlayerAudio<'_, T> { +impl Audio for Sequencer { fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - let model = &mut self.0; - let note_buf = &mut self.1; - let midi_buf = &mut self.2; - // Clear output buffer(s) - model.clear(scope, midi_buf, false); - // Write chunk of clip to output, handle switchover - if model.play(scope, note_buf, midi_buf) { - model.switchover(scope, note_buf, midi_buf); + if self.clock().is_rolling() { + self.process_rolling(scope) + } else { + self.process_stopped(scope) } - if !model.midi_ins().is_empty() { - if model.recording() || model.monitoring() { - // Record and/or monitor input - model.record(scope, midi_buf) - } else if model.midi_outs().is_empty() && model.monitoring() { - // Monitor input to output - model.monitor(scope, midi_buf) - } - } - // Write to output port(s) - model.write(scope, midi_buf); - Control::Continue } } -pub trait MidiSequencer: MidiRecorder + MidiPlayer + Send + Sync {} - -impl MidiSequencer for Sequencer {} - -pub trait MidiRecorder: HasClock + HasPlayClip + HasMidiIns { - fn notes_in (&self) -> &Arc>; - - fn recording (&self) -> bool; - - fn recording_mut (&mut self) -> &mut bool; - - fn toggle_record (&mut self) { - *self.recording_mut() = !self.recording(); +impl Sequencer { + fn process_rolling (&mut self, scope: &ProcessScope) -> Control { + self.process_clear(scope, false); + // Write chunk of clip to output, handle switchover + if self.process_playback(scope) { + self.process_switchover(scope); + } + // Monitor input to output + self.process_monitoring(scope); + // Record and/or monitor input + self.process_recording(scope); + // Emit contents of MIDI buffers to JACK MIDI output ports. + self.midi_outs_emit(scope); + Control::Continue } - - fn monitoring (&self) -> bool; - - fn monitoring_mut (&mut self) -> &mut bool; - - fn toggle_monitor (&mut self) { - *self.monitoring_mut() = !self.monitoring(); + fn process_stopped (&mut self, scope: &ProcessScope) -> Control { + if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { + self.process_monitoring(scope) + } + Control::Continue } - - fn overdub (&self) -> bool; - - fn overdub_mut (&mut self) -> &mut bool; - - fn toggle_overdub (&mut self) { - *self.overdub_mut() = !self.overdub(); - } - - fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { - // For highlighting keys and note repeat - let notes_in = self.notes_in().clone(); + fn process_monitoring (&mut self, scope: &ProcessScope) { + let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat let monitoring = self.monitoring(); - for input in self.midi_ins_mut().iter() { - for (sample, event, bytes) in parse_midi_input(input.port().iter(scope)) { + for input in self.midi_ins.iter() { + for (sample, event, bytes) in input.parsed(scope) { if let LiveEvent::Midi { message, .. } = event { if monitoring { - midi_buf[sample].push(bytes.to_vec()); + self.midi_buf[sample].push(bytes.to_vec()); } // FIXME: don't lock on every event! update_keys(&mut notes_in.write().unwrap(), &message); @@ -84,28 +47,143 @@ pub trait MidiRecorder: HasClock + HasPlayClip + HasMidiIns { } } } - - fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); + for frame in &mut self.midi_buf_mut()[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(self.midi_buf_mut()); + } + for port in self.midi_outs_mut().iter_mut() { + // Clear output buffer(s) + port.buffer_clear(scope, false); + } + } + fn process_recording (&mut self, scope: &ProcessScope) { if self.monitoring() { - self.monitor(scope, midi_buf); + self.monitor(scope); } - if !self.clock().is_rolling() { - return - } - if let Some((started, ref clip)) = self.play_clip().clone() { - self.record_clip(scope, started, clip, midi_buf); + if let Some((started, ref clip)) = self.play_clip.clone() { + self.record_clip(scope, started, clip); } if let Some((_start_at, _clip)) = &self.next_clip() { self.record_next(); } } + fn process_playback (&mut self, scope: &ProcessScope) -> bool { + // If a clip is playing, write a chunk of MIDI events from it to the output buffer. + // If no clip is playing, prepare for switchover immediately. + if let Some((started, clip)) = &self.play_clip { + // Length of clip, to repeat or stop on end. + let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); + // Index of first sample to populate. + let offset = self.clock().get_sample_offset(scope, &started); + // Write MIDI events from clip at sample offsets corresponding to pulses. + for (sample, pulse) in self.clock().get_pulses(scope, offset) { + // If a next clip is enqueued, and we're past the end of the current one, + // break the loop here (FIXME count pulse correctly) + let past_end = if clip.is_some() { pulse >= length } else { true }; + // Is it time for switchover? + if self.next_clip().is_some() && past_end { + return true + } + // If there's a currently playing clip, output notes from it to buffer: + if let Some(ref clip) = clip { + // Source clip from which the MIDI events will be taken. + let clip = clip.read().unwrap(); + // Clip with zero length is not processed + if clip.length > 0 { + // Current pulse index in source clip + let pulse = pulse % clip.length; + // Output each MIDI event from clip at appropriate frames of output buffer: + for message in clip.notes[pulse].iter() { + for port in self.midi_outs.iter_mut() { + port.buffer_write(sample, LiveEvent::Midi { + channel: 0.into(), /* TODO */ + message: *message + }); + } + } + } + } + } + false + } else { + true + } + } + /// Handle switchover from current to next playing clip. + fn process_switchover (&mut self, scope: &ProcessScope) { + let midi_buf = self.midi_buf_mut(); + let sample0 = scope.last_frame_time() as usize; + //let samples = scope.n_frames() as usize; + if let Some((start_at, clip)) = &self.next_clip() { + let start = start_at.sample.get() as usize; + let sample = self.clock().started.read().unwrap() + .as_ref().unwrap().sample.get() as usize; + // If it's time to switch to the next clip: + if start <= sample0.saturating_sub(sample) { + // Samples elapsed since clip was supposed to start + let _skipped = sample0 - start; + // Switch over to enqueued clip + let started = Moment::from_sample(self.clock().timebase(), start as f64); + // Launch enqueued clip + *self.play_clip_mut() = Some((started, clip.clone())); + // Unset enqueuement (TODO: where to implement looping?) + *self.next_clip_mut() = None; + // Fill in remaining ticks of chunk from next clip. + self.process_playback(scope); + } + } + } +} + +pub trait HasMidiBuffers { + fn note_buf_mut (&mut self) -> &mut Vec; + fn midi_buf_mut (&mut self) -> &mut Vec>>; +} + +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 + } +} + +pub trait MidiMonitor: HasMidiIns + HasMidiBuffers { + fn notes_in (&self) -> &Arc>; + fn monitoring (&self) -> bool; + fn monitoring_mut (&mut self) -> &mut bool; + fn toggle_monitor (&mut self) { + *self.monitoring_mut() = !self.monitoring(); + } + fn monitor (&mut self, scope: &ProcessScope) { + } +} + +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>>, - _midi_buf: &mut Vec>> + scope: &ProcessScope, + started: Moment, + clip: &Option>>, ) { if let Some(clip) = clip { let sample0 = scope.last_frame_time() as usize; @@ -113,7 +191,7 @@ pub trait MidiRecorder: HasClock + HasPlayClip + HasMidiIns { let _recording = self.recording(); let timebase = self.clock().timebase().clone(); let quant = self.clock().quant.get(); - let mut clip = clip.write().unwrap(); + 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)) { @@ -133,173 +211,4 @@ pub trait MidiRecorder: HasClock + HasPlayClip + HasMidiIns { fn record_next (&mut self) { // TODO switch to next clip and record into it } - -} - -pub trait MidiPlayer: HasPlayClip + HasClock + HasMidiOuts { - - fn notes_out (&self) -> &Arc>; - - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - fn clear ( - &mut self, scope: &ProcessScope, out: &mut [Vec>], reset: bool - ) { - let n_frames = (scope.n_frames() as usize).min(out.len()); - for frame in &mut out[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(out); - } - } - - /// Output notes from clip to MIDI output ports. - fn play ( - &mut self, scope: &ProcessScope, note_buf: &mut Vec, out: &mut [Vec>] - ) -> bool { - if !self.clock().is_rolling() { - return false - } - // If a clip is playing, write a chunk of MIDI events from it to the output buffer. - // If no clip is playing, prepare for switchover immediately. - if let Some((started, clip)) = self.play_clip() { - self.play_chunk(scope, note_buf, out, started, clip) - } else { - true - } - } - - fn play_chunk ( - &self, - scope: &ProcessScope, - note_buf: &mut Vec, - out: &mut [Vec>], - started: &Moment, - clip: &Option>> - ) -> bool { - // Index of first sample to populate. - let offset = self.get_sample_offset(scope, started); - // Notes active during current chunk. - let notes = &mut self.notes_out().write().unwrap(); - // Length of clip. - let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); - // Write MIDI events from clip at sample offsets corresponding to pulses. - for (sample, pulse) in self.get_pulses(scope, offset) { - // If a next clip is enqueued, and we're past the end of the current one, - // break the loop here (FIXME count pulse correctly) - let past_end = if clip.is_some() { pulse >= length } else { true }; - // Is it time for switchover? - if self.next_clip().is_some() && past_end { - return true - } - // If there's a currently playing clip, output notes from it to buffer: - if let Some(ref clip) = clip { - Self::play_pulse(clip, pulse, sample, note_buf, out, notes) - } - } - false - } - - /// Get index of first sample to populate. - /// - /// Greater than 0 means that the first pulse of the clip - /// falls somewhere in the middle of the chunk. - fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ - (scope.last_frame_time() as usize).saturating_sub( - started.sample.get() as usize + - self.clock().started.read().unwrap().as_ref().unwrap().sample.get() as usize - ) - } - - // Get iterator that emits sample paired with pulse. - // - // * Sample: index into output buffer at which to write MIDI event - // * Pulse: index into clip from which to take the MIDI event - // - // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. - fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> TicksIterator { - self.clock().timebase().pulses_between_samples( - offset, offset + scope.n_frames() as usize) - } - - /// Handle switchover from current to next playing clip. - fn switchover ( - &mut self, scope: &ProcessScope, note_buf: &mut Vec, out: &mut [Vec>] - ) { - if !self.clock().is_rolling() { - return - } - let sample0 = scope.last_frame_time() as usize; - //let samples = scope.n_frames() as usize; - if let Some((start_at, clip)) = &self.next_clip() { - let start = start_at.sample.get() as usize; - let sample = self.clock().started.read().unwrap() - .as_ref().unwrap().sample.get() as usize; - // If it's time to switch to the next clip: - if start <= sample0.saturating_sub(sample) { - // Samples elapsed since clip was supposed to start - let _skipped = sample0 - start; - // Switch over to enqueued clip - let started = Moment::from_sample(self.clock().timebase(), start as f64); - // Launch enqueued clip - *self.play_clip_mut() = Some((started, clip.clone())); - // Unset enqueuement (TODO: where to implement looping?) - *self.next_clip_mut() = None; - // Fill in remaining ticks of chunk from next clip. - self.play(scope, note_buf, out); - } - } - } - - fn play_pulse ( - clip: &RwLock, - pulse: usize, - sample: usize, - note_buf: &mut Vec, - out: &mut [Vec>], - notes: &mut [bool;128] - ) { - // Source clip from which the MIDI events will be taken. - let clip = clip.read().unwrap(); - // Clip with zero length is not processed - if clip.length > 0 { - // Current pulse index in source clip - let pulse = pulse % clip.length; - // Output each MIDI event from clip at appropriate frames of output buffer: - for message in clip.notes[pulse].iter() { - // Clear output buffer for this MIDI event. - note_buf.clear(); - // TODO: support MIDI channels other than CH1. - let channel = 0.into(); - // Serialize MIDI event into message buffer. - LiveEvent::Midi { channel, message: *message } - .write(note_buf) - .unwrap(); - // Append serialized message to output buffer. - out[sample].push(note_buf.clone()); - // Update the list of currently held notes. - update_keys(&mut*notes, message); - } - } - } - - /// Write a chunk of MIDI data from the output buffer to all assigned output ports. - fn write (&mut self, scope: &ProcessScope, out: &[Vec>]) { - let samples = scope.n_frames() as usize; - for port in self.midi_outs_mut().iter_mut() { - Self::write_port(&mut port.port_mut().writer(scope), samples, out) - } - } - - /// Write a chunk of MIDI data from the output buffer to an output port. - fn write_port (writer: &mut MidiWriter, samples: usize, out: &[Vec>]) { - for (time, events) in out.iter().enumerate().take(samples) { - for bytes in events.iter() { - writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ - panic!("Failed to write MIDI data: {bytes:?}"); - }); - } - } - } } diff --git a/crates/device/src/sequencer/seq_model.rs b/crates/device/src/sequencer/seq_model.rs index a4dd982d..5524dc04 100644 --- a/crates/device/src/sequencer/seq_model.rs +++ b/crates/device/src/sequencer/seq_model.rs @@ -2,17 +2,17 @@ use crate::*; impl> HasSequencer for T { - fn sequencer (&self) -> &impl MidiSequencer { + fn sequencer (&self) -> &Sequencer { self.get() } - fn sequencer_mut (&mut self) -> &mut impl MidiSequencer { + fn sequencer_mut (&mut self) -> &mut Sequencer { self.get_mut() } } pub trait HasSequencer { - fn sequencer (&self) -> &impl MidiSequencer; - fn sequencer_mut (&mut self) -> &mut impl MidiSequencer; + fn sequencer (&self) -> &Sequencer; + fn sequencer_mut (&mut self) -> &mut Sequencer; } /// Contains state for playing a clip @@ -41,6 +41,8 @@ pub struct Sequencer { pub notes_out: Arc>, /// MIDI output buffer pub note_buf: Vec, + /// MIDI output buffer + pub midi_buf: Vec>>, } impl Default for Sequencer { @@ -55,6 +57,7 @@ impl Default for Sequencer { notes_in: RwLock::new([false;128]).into(), notes_out: RwLock::new([false;128]).into(), note_buf: vec![0;8], + midi_buf: vec![], reset: true, midi_ins: vec![], @@ -80,14 +83,10 @@ impl Sequencer { midi_outs: vec![JackMidiOut::new(jack, format!("{}/M", name.as_ref()), midi_to)?, ], play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), clock, - note_buf: vec![0;8], reset: true, - recording: false, - monitoring: false, - overdub: false, - next_clip: None, notes_in: RwLock::new([false;128]).into(), notes_out: RwLock::new([false;128]).into(), + ..Default::default() }) } } @@ -106,12 +105,9 @@ has!(Clock: |self: Sequencer|self.clock); has!(Vec: |self:Sequencer| self.midi_ins); has!(Vec: |self:Sequencer| self.midi_outs); -impl MidiRecorder for Sequencer { - fn recording (&self) -> bool { - self.recording - } - fn recording_mut (&mut self) -> &mut bool { - &mut self.recording +impl MidiMonitor for Sequencer { + fn notes_in (&self) -> &Arc> { + &self.notes_in } fn monitoring (&self) -> bool { self.monitoring @@ -119,21 +115,21 @@ impl MidiRecorder for Sequencer { 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 } - fn notes_in (&self) -> &Arc> { - &self.notes_in - } -} - -impl MidiPlayer for Sequencer { - fn notes_out (&self) -> &Arc> { - &self.notes_out - } } impl HasPlayClip for Sequencer { diff --git a/crates/engine/src/jack/jack_port.rs b/crates/engine/src/jack/jack_port.rs index 85136cb9..8d0d852d 100644 --- a/crates/engine/src/jack/jack_port.rs +++ b/crates/engine/src/jack/jack_port.rs @@ -189,7 +189,7 @@ macro_rules! impl_port { type Pair = $Pair; fn port (&self) -> &Port<$Spec> { &self.port } } - impl JackPortConnect<&str> for $Name { + impl JackPortConnect<&str> for $Name { fn connect_to (&self, to: &str) -> Usually { self.with_client(|c|if let Some(ref port) = c.port_by_name(to.as_ref()) { self.connect_to(port) @@ -233,5 +233,3 @@ impl_port!(JackAudioIn: AudioIn -> AudioOut |j, n|j.register_port::(n impl_port!(JackAudioOut: AudioOut -> AudioIn |j, n|j.register_port::(n)); impl_port!(JackMidiIn: MidiIn -> MidiOut |j, n|j.register_port::(n)); - -impl_port!(JackMidiOut: MidiOut -> MidiIn |j, n|j.register_port::(n)); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index e9bcf677..55b8d382 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -18,6 +18,16 @@ pub trait Has: Send + Sync { fn get_mut (&mut self) -> &mut T; } +pub trait MaybeHas: Send + Sync { + fn get (&self) -> Option<&T>; +} + +impl>> MaybeHas for U { + fn get (&self) -> Option<&T> { + Has::>::get(self).as_ref() + } +} + #[macro_export] macro_rules! has { ($T:ty: |$self:ident : $S:ty| $x:expr) => { impl Has<$T> for $S { diff --git a/crates/engine/src/midi.rs b/crates/engine/src/midi.rs index e6a656b0..c3588d1c 100644 --- a/crates/engine/src/midi.rs +++ b/crates/engine/src/midi.rs @@ -9,17 +9,9 @@ pub use ::midly::{ live::*, }; -mod midi_in; pub use self::midi_in::*; -mod midi_out; pub use self::midi_out::*; - -/// Update notes_in array -pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { - match message { - MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } - MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, - _ => {} - } -} +mod midi_in; pub use self::midi_in::*; +mod midi_out; pub use self::midi_out::*; +mod midi_hold; pub use self::midi_hold::*; /// Return boxed iterator of MIDI events pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>) -> Box, &'a [u8])> + 'a> { diff --git a/crates/engine/src/midi/midi_hold.rs b/crates/engine/src/midi/midi_hold.rs new file mode 100644 index 00000000..0717e90c --- /dev/null +++ b/crates/engine/src/midi/midi_hold.rs @@ -0,0 +1,10 @@ +use crate::*; + +/// Update notes_in array +pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { + match message { + MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } + MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, + _ => {} + } +} diff --git a/crates/engine/src/midi/midi_in.rs b/crates/engine/src/midi/midi_in.rs index 27cbd76e..027025d0 100644 --- a/crates/engine/src/midi/midi_in.rs +++ b/crates/engine/src/midi/midi_in.rs @@ -1,5 +1,11 @@ use crate::*; +impl JackMidiIn { + pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { + parse_midi_input(self.port().iter(scope)) + } +} + #[tengri_proc::command(JackMidiIn)] impl MidiInputCommand { } diff --git a/crates/engine/src/midi/midi_out.rs b/crates/engine/src/midi/midi_out.rs index 89a22296..514f6487 100644 --- a/crates/engine/src/midi/midi_out.rs +++ b/crates/engine/src/midi/midi_out.rs @@ -1,5 +1,151 @@ use crate::*; +#[derive(Debug)] pub struct JackMidiOut { + /// Handle to JACK client, for receiving reconnect events. + jack: Jack, + /// Port name + name: Arc, + /// Port handle. + port: Port, + /// List of ports to connect to. + conn: Vec, + /// List of currently held notes. + held: Arc>, + /// Buffer + note_buffer: Vec, + /// Buffer + output_buffer: Vec>>, +} + +has!(Jack: |self: JackMidiOut|self.jack); + +impl JackMidiOut { + pub fn new (jack: &Jack, name: impl AsRef, connect: &[PortConnect]) + -> Usually + { + let jack = jack.clone(); + let port = jack.register_port::(name.as_ref())?; + let name = name.as_ref().into(); + let conn = connect.to_vec(); + let port = Self { + jack, + port, + name, + conn, + held: Arc::new([false;128].into()), + note_buffer: vec![0;8], + output_buffer: vec![vec![];65536], + }; + port.connect_to_matching()?; + Ok(port) + } + pub fn name (&self) -> &Arc { + &self.name + } + pub fn port (&self) -> &Port { + &self.port + } + pub fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + pub fn into_port (self) -> Port { + self.port + } + pub fn close (self) -> Usually<()> { + let Self { jack, port, .. } = self; + Ok(jack.with_client(|client|client.unregister_port(port))?) + } + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); + for frame in &mut self.output_buffer[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(&mut self.output_buffer); + } + } + /// Write a note to the output buffer + pub fn buffer_write <'a> ( + &'a mut self, + sample: usize, + event: LiveEvent, + ) { + self.note_buffer.fill(0); + event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); + self.output_buffer[sample].push(self.note_buffer.clone()); + // Update the list of currently held notes. + if let LiveEvent::Midi { ref message, .. } = event { + update_keys(&mut*self.held.write().unwrap(), message); + } + } + /// Write a chunk of MIDI data from the output buffer to the output port. + pub fn buffer_emit (&mut self, scope: &ProcessScope) { + let samples = scope.n_frames() as usize; + let mut writer = self.port.writer(scope); + for (time, events) in self.output_buffer.iter().enumerate().take(samples) { + for bytes in events.iter() { + writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ + panic!("Failed to write MIDI data: {bytes:?}"); + }); + } + } + } +} + +impl AsRef> for JackMidiOut { + fn as_ref (&self) -> &Port { + &self.port + } +} + +impl JackPort for JackMidiOut { + type Port = MidiOut; + type Pair = MidiIn; + fn port (&self) -> &Port { &self.port } +} + +impl JackPortConnect<&str> for JackMidiOut { + fn connect_to (&self, to: &str) -> Usually { + self.with_client(|c|if let Some(ref port) = c.port_by_name(to.as_ref()) { + self.connect_to(port) + } else { + Ok(Missing) + }) + } +} + +impl JackPortConnect<&Port> for JackMidiOut { + fn connect_to (&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 + })) + } +} + +impl JackPortConnect<&Port> for JackMidiOut { + fn connect_to (&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 + })) + } +} + +impl JackPortAutoconnect for JackMidiOut { + fn conn (&self) -> &[PortConnect] { + &self.conn + } +} + #[tengri_proc::command(JackMidiOut)] impl MidiOutputCommand { } @@ -29,6 +175,11 @@ pub trait HasMidiOuts { data }) } + fn midi_outs_emit (&mut self, scope: &ProcessScope) { + for port in self.midi_outs_mut().iter_mut() { + port.buffer_emit(scope) + } + } } /// Trail for thing that may gain new MIDI ports. diff --git a/deps/tengri b/deps/tengri index bad20f50..a9619ab9 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit bad20f5037dc22d572a8381840fab871ce65f565 +Subproject commit a9619ab9cea40aed3ec30483f4a7db03ca7fec9d From 8df49850ae1cd8aa86c1097ca44115c0078ddb9c Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 14 May 2025 14:56:12 +0300 Subject: [PATCH 04/12] down to 3 errors --- crates/app/src/api.rs | 16 +++++- crates/device/src/arranger/arranger_api.rs | 10 ---- crates/device/src/arranger/arranger_clip.rs | 34 ------------ crates/device/src/arranger/arranger_model.rs | 54 ++++++++++++++++++- crates/device/src/arranger/arranger_select.rs | 35 +++--------- crates/device/src/arranger/arranger_tracks.rs | 8 ++- crates/engine/src/lib.rs | 8 +++ 7 files changed, 84 insertions(+), 81 deletions(-) diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index 17ef811a..430a520b 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -47,13 +47,27 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm Ok(None) } fn select (app: &mut App, selection: Selection) -> Perhaps { - app.select(selection); + app.arranger.select(selection); + if let Some(ref mut editor) = app.editor { + editor.set_clip(match app.arranger.selected { + Some(Selection::TrackClip { track, scene }) + if let Some(Some(Some(clip))) = app + .arranger + .scenes.get(scene) + .map(|s|s.clips.get(track)) + => + Some(clip), + _ => + None + }); + } Ok(None) //("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) { //(0, 0) => Self::Select(Selection::Mix), //(t, 0) => Self::Select(Selection::Track(t)), //(0, s) => Self::Select(Selection::Scene(s)), //(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) + // autoedit: load focused clip in editor. } fn stop_all (app: &mut App) -> Perhaps { app.stop_all(); diff --git a/crates/device/src/arranger/arranger_api.rs b/crates/device/src/arranger/arranger_api.rs index 9034baca..6f3210b9 100644 --- a/crates/device/src/arranger/arranger_api.rs +++ b/crates/device/src/arranger/arranger_api.rs @@ -14,16 +14,6 @@ impl ArrangementCommand { /// Set the selection fn select (arranger: &mut Arrangement, s: Option) -> Perhaps { arranger.selected = s; - // autoedit: load focused clip in editor. - if let Some(ref mut editor) = arranger.editor { - editor.set_clip(match arranger.selected { - Some(Selection::TrackClip { track, scene }) - if let Some(Some(Some(clip))) = arranger - .scenes.get(scene) - .map(|s|s.clips.get(track)) => Some(clip), - _ => None - }); - } Ok(None) } /// Launch a clip or scene diff --git a/crates/device/src/arranger/arranger_clip.rs b/crates/device/src/arranger/arranger_clip.rs index b0ad719a..98dd7ba7 100644 --- a/crates/device/src/arranger/arranger_clip.rs +++ b/crates/device/src/arranger/arranger_clip.rs @@ -33,38 +33,4 @@ impl ClipCommand { impl Arrangement { - /// Put a clip in a slot - pub(crate) fn clip_put ( - &mut self, track: usize, scene: usize, clip: Option>> - ) -> Option>> { - let old = self.scenes[scene].clips[track].clone(); - self.scenes[scene].clips[track] = clip; - old - } - - /// Change the color of a clip, returning the previous one - pub(crate) fn clip_set_color ( - &self, track: usize, scene: usize, color: ItemTheme - ) -> Option { - self.scenes[scene].clips[track].as_ref().map(|clip|{ - let mut clip = clip.write().unwrap(); - let old = clip.color.clone(); - clip.color = color.clone(); - panic!("{color:?} {old:?}"); - old - }) - } - - /// Get the active clip - pub(crate) fn clip (&self) -> Option>> { - self.scene()?.clips.get(self.selected.track()?)?.clone() - } - - /// Toggle looping for the active clip - pub(crate) fn toggle_loop (&mut self) { - if let Some(clip) = self.clip() { - clip.write().unwrap().toggle_loop() - } - } - } diff --git a/crates/device/src/arranger/arranger_model.rs b/crates/device/src/arranger/arranger_model.rs index d2f3246c..7d59ed67 100644 --- a/crates/device/src/arranger/arranger_model.rs +++ b/crates/device/src/arranger/arranger_model.rs @@ -119,16 +119,66 @@ impl Arrangement { 2 //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } + /// Get the active track + fn get_track (&self) -> Option<&Track> { + let index = self.selection()?.track()?; + Has::>::get(self).get(index) + } + /// Get a mutable reference to the active track + fn get_track_mut (&mut self) -> Option<&mut Track> { + let index = self.selection()?.track()?; + Has::>::get_mut(self).get_mut(index) + } + /// Get the active scene + fn get_scene (&self) -> Option<&Scene> { + let index = self.selection()?.scene()?; + Has::>::get(self).get(index) + } + /// Get a mutable reference to the active scene + 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 + fn get_clip (&self) -> Option>> { + self.get_scene()?.clips.get(self.selection()?.track()?)?.clone() + } + /// Put a clip in a slot + pub(crate) fn clip_put ( + &mut self, track: usize, scene: usize, clip: Option>> + ) -> Option>> { + let old = self.scenes[scene].clips[track].clone(); + self.scenes[scene].clips[track] = clip; + old + } + /// Change the color of a clip, returning the previous one + pub(crate) fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) + -> Option + { + self.scenes[scene].clips[track].as_ref().map(|clip|{ + let mut clip = clip.write().unwrap(); + let old = clip.color.clone(); + clip.color = color.clone(); + panic!("{color:?} {old:?}"); + old + }) + } + /// Toggle looping for the active clip + pub(crate) fn toggle_loop (&mut self) { + if let Some(clip) = self.get_clip() { + clip.write().unwrap().toggle_loop() + } + } } #[cfg(feature = "sampler")] impl Arrangement { /// Get the first sampler of the active track pub fn sampler (&self) -> Option<&Sampler> { - self.track().map(|t|t.sampler(0)).flatten() + self.get_track()?.sampler(0) } /// Get the first sampler of the active track pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.track_mut().map(|t|t.sampler_mut(0)).flatten() + self.get_track_mut()?.sampler_mut(0) } } diff --git a/crates/device/src/arranger/arranger_select.rs b/crates/device/src/arranger/arranger_select.rs index 8330faf2..81a2f7a1 100644 --- a/crates/device/src/arranger/arranger_select.rs +++ b/crates/device/src/arranger/arranger_select.rs @@ -1,40 +1,17 @@ use crate::*; -impl> HasSelection for T {} +impl>> HasSelection for T {} -pub trait HasSelection: Has { - fn selection (&self) -> &Selection { - self.get() +pub trait HasSelection: Has> { + fn selection (&self) -> Option<&Selection> { + self.get().as_ref() } - fn selection_mut (&mut self) -> &mut Selection { + fn selection_mut (&mut self) -> &mut Option { self.get_mut() } } -impl Has> for Arrangement { - fn get (&self) -> &Option { - let selection: Selection = Has::>::get(self)?; - let index: Option = selection.track()?; - let track: Track = Has::>::get(self).get(index)?; - } - fn get_mut (&mut self) -> &mut Option { - Has::>::get(self) - .and_then(|selection|selection.track()) - .and_then(|index|Has::>::get_mut(self).get_mut(index)) - } -} - -impl Has> for Arrangement { - fn get (&self) -> &Option { - Has::>::get(self) - .and_then(|selection|selection.track()) - .and_then(|index|Has::>::get(self).get(index)) - } - fn get_mut (&mut self) -> &mut Option { - Has::>::get(self) - .and_then(|selection|selection.track()) - .and_then(|index|Has::>::get_mut(self).get_mut(index).as_deref()) - } +impl Arrangement { } /// Represents the current user selection in the arranger diff --git a/crates/device/src/arranger/arranger_tracks.rs b/crates/device/src/arranger/arranger_tracks.rs index 7863a9e7..56fce892 100644 --- a/crates/device/src/arranger/arranger_tracks.rs +++ b/crates/device/src/arranger/arranger_tracks.rs @@ -81,11 +81,9 @@ impl<'a> ArrangerView<'a> { } } -impl AddTrack for T {} - -pub trait AddTrack: HasTracks + HasScenes + HasClock + HasJack { +impl Arrangement { /// Add multiple tracks - fn tracks_add ( + pub fn tracks_add ( &mut self, count: usize, width: Option, @@ -106,7 +104,7 @@ pub trait AddTrack: HasTracks + HasScenes + HasClock + HasJack { } /// Add a track - fn track_add ( + pub fn track_add ( &mut self, name: Option<&str>, color: Option, diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 55b8d382..8f891e4c 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -37,6 +37,14 @@ impl>> MaybeHas for U { }; } +#[macro_export] macro_rules! as_ref { + ($T:ty: |$self:ident : $S:ty| $x:expr) => { + impl AsRef<$T> for $S { + fn as_ref (&$self) -> &$T { &$x } + } + }; +} + pub trait HasN: Send + Sync { fn get_nth (&self, key: usize) -> &T; fn get_nth_mut (&mut self, key: usize) -> &mut T; From 254e19db0d66965712e0c886f3cdf274e6c30d78 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 14 May 2025 15:08:58 +0300 Subject: [PATCH 05/12] down to 2 weirdest errors --- crates/device/src/arranger/arranger_api.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/device/src/arranger/arranger_api.rs b/crates/device/src/arranger/arranger_api.rs index 6f3210b9..6180a5d1 100644 --- a/crates/device/src/arranger/arranger_api.rs +++ b/crates/device/src/arranger/arranger_api.rs @@ -106,28 +106,32 @@ impl ArrangementCommand { //TODO:Ok(Some(Self::TrackAdd ( index, track: Some(deleted_track) }) } fn midi_in (arranger: &mut Arrangement, input: MidiInputCommand) -> Perhaps { - todo!("delegate") + todo!("delegate"); + Ok(None) } fn midi_in_add (arranger: &mut Arrangement) -> Perhaps { arranger.midi_in_add()?; Ok(None) } fn midi_out (arranger: &mut Arrangement, input: MidiOutputCommand) -> Perhaps { - todo!("delegate") + todo!("delegate"); + Ok(None) } fn midi_out_add (arranger: &mut Arrangement) -> Perhaps { arranger.midi_out_add()?; Ok(None) } fn device (arranger: &mut Arrangement, input: DeviceCommand) -> Perhaps { - todo!("delegate") + todo!("delegate"); + Ok(None) } fn device_add (arranger: &mut Arrangement, i: usize) -> Perhaps { - arranger.device_add(i); + todo!("delegate"); Ok(None) } fn scene (arranger: &mut Arrangement, scene: SceneCommand) -> Perhaps { - todo!("delegate") + todo!("delegate"); + Ok(None) } fn scene_add (arranger: &mut Arrangement) -> Perhaps { let index = arranger.scene_add(None, None)?.0; From f3c67f95b517b11c6d221498e8d2572a3f9c20ad Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 14 May 2025 15:13:25 +0300 Subject: [PATCH 06/12] down to 1 weirdest error --- crates/device/src/arranger/arranger_view.rs | 141 ++++++++++---------- 1 file changed, 67 insertions(+), 74 deletions(-) diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs index e6213432..37b8e593 100644 --- a/crates/device/src/arranger/arranger_view.rs +++ b/crates/device/src/arranger/arranger_view.rs @@ -86,7 +86,7 @@ impl<'a> Content for ArrangerView<'a> { let bg = |x|Tui::bg(Reset, x); //let track_scroll = |x|Bsp::s(&self.track_scroll, x); //let scene_scroll = |x|Bsp::e(&self.scene_scroll, x); - outs(tracks(devices(ins(bg(self.scenes(None)))))) + outs(tracks(devices(ins(bg(self.scenes(&None)))))) } } @@ -150,7 +150,7 @@ impl<'a> ArrangerView<'a> { pub(crate) const H_EDITOR: usize = 15; /// Render scenes with clips - pub(crate) fn scenes (&'a self, editor: Option) -> impl Content + 'a { + pub(crate) fn scenes (&'a self, editor: &'a Option) -> impl Content + 'a { /// A scene with size and color. type SceneWithColor<'a> = (usize, &'a Scene, usize, usize, Option); let Self { @@ -163,85 +163,78 @@ impl<'a> ArrangerView<'a> { let selection = Has::>::get(self.arrangement); let selected_track = selection.map(|s|s.track()).flatten(); let selected_scene = selection.map(|s|s.scene()).flatten(); - - let scenes_with_scene_colors = ||HasScenes::scenes_with_sizes(self.arrangement, - *is_editing, - Self::H_SCENE, - Self::H_EDITOR, - selected_track, - selected_scene, - ).map_while(|(s, scene, y1, y2)|if y2 as u16 > *scenes_height { - None - } else { - Some((s, scene, y1, y2, if s == 0 { - None - } else { - Some(arrangement.scenes()[s-1].color) - })) - }); - - let scene_header = |(s, scene, y1, y2, previous): SceneWithColor, _|{ - let height = (1 + y2 - y1) as u16; - let name = Some(scene.name.clone()); - let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⯈ ", name)))); - let same_track = true; - let selected = same_track && *scene_selected == Some(s); - let neighbor = same_track && s > 0 && *scene_selected == Some(s - 1); - let is_last = *scene_last == s; - let theme = scene.color; - let fg = theme.lightest.rgb; - let bg = if selected { theme.light } else { theme.base }.rgb; - let hi = if let Some(previous) = previous { - if neighbor { - previous.light.rgb - } else { - previous.base.rgb - } - } else { - Reset - }; - let lo = if is_last { - Reset - } else if selected { - theme.light.rgb - } else { - theme.base.rgb - }; - Fill::x(map_south(y1 as u16, height, Fixed::y(height, Phat { - width: 0, height: 0, content, colors: [fg, bg, hi, lo] - }))) - }; - - let scenes_with_track_colors = |track: usize| arrangement.scenes_with_sizes( - self.is_editing, - Self::H_SCENE, - Self::H_EDITOR, - selected_track, - selected_scene, - ).map_while(|(s, scene, y1, y2)|if y2 as u16 > self.scenes_height { - None - } else { - Some((s, scene, y1, y2, if s == 0 { - None - } else { - Some(self.arrangement.scenes[s-1].clips[track].as_ref() - .map(|c|c.read().unwrap().color) - .unwrap_or(ItemTheme::G[32])) - })) - }); - Tryptich::center(*scenes_height) .left(*width_side, Map::new( - ||scenes_with_scene_colors(), - scene_header)) + move||arrangement.scenes_with_sizes( + *is_editing, + Self::H_SCENE, + Self::H_EDITOR, + selected_track, + selected_scene, + ).map_while(|(s, scene, y1, y2)|if y2 as u16 > *scenes_height { + None + } else { + Some((s, scene, y1, y2, if s == 0 { + None + } else { + Some(arrangement.scenes()[s-1].color) + })) + }), + move |(s, scene, y1, y2, previous): SceneWithColor, _|{ + let height = (1 + y2 - y1) as u16; + let name = Some(scene.name.clone()); + let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⯈ ", name)))); + let same_track = true; + let selected = same_track && *scene_selected == Some(s); + let neighbor = same_track && s > 0 && *scene_selected == Some(s - 1); + let is_last = *scene_last == s; + let theme = scene.color; + let fg = theme.lightest.rgb; + let bg = if selected { theme.light } else { theme.base }.rgb; + let hi = if let Some(previous) = previous { + if neighbor { + previous.light.rgb + } else { + previous.base.rgb + } + } else { + Reset + }; + let lo = if is_last { + Reset + } else if selected { + theme.light.rgb + } else { + theme.base.rgb + }; + Fill::x(map_south(y1 as u16, height, Fixed::y(height, Phat { + width: 0, height: 0, content, colors: [fg, bg, hi, lo] + }))) + })) .middle(*width_mid, per_track( *width_mid, ||self.tracks_with_sizes_scrolled(), - |track_index, track|Map::new( - ||scenes_with_track_colors(track_index), - |(s, scene, y1, y2, previous): SceneWithColor<'a>, _|{ + move|track_index, track|Map::new( + move||arrangement.scenes_with_sizes( + self.is_editing, + Self::H_SCENE, + Self::H_EDITOR, + selected_track, + selected_scene, + ).map_while(move|(s, scene, y1, y2)|if y2 as u16 > self.scenes_height { + None + } else { + Some((s, scene, y1, y2, if s == 0 { + None + } else { + Some(self.arrangement.scenes[s-1].clips[track_index].as_ref() + .map(|c|c.read().unwrap().color) + .unwrap_or(ItemTheme::G[32])) + })) + }), + move|(s, scene, y1, y2, previous): SceneWithColor<'a>, _|{ let (name, theme) = if let Some(clip) = &scene.clips[track_index] { let clip = clip.read().unwrap(); (Some(clip.name.clone()), clip.color) From 57eff509733394bb070d9ea3f80d2bc7cbaf537c Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 14 May 2025 15:16:43 +0300 Subject: [PATCH 07/12] wip: back to 89 errors --- crates/app/src/api.rs | 16 ++------------ crates/app/src/model.rs | 5 ++++- crates/app/src/view.rs | 24 ++++++++++----------- crates/device/src/arranger/arranger_api.rs | 16 +++++++------- crates/device/src/arranger/arranger_view.rs | 6 +----- 5 files changed, 27 insertions(+), 40 deletions(-) diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index 430a520b..f2fe97f5 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -79,20 +79,8 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm .transpose()? .flatten()) } - fn scene (app: &mut App, command: SceneCommand) -> Perhaps { - Ok(command.delegate(app, |command|Self::Scene{command})?) - } - fn track (app: &mut App, command: TrackCommand) -> Perhaps { - Ok(command.delegate(app, |command|Self::Track{command})?) - } - fn input (app: &mut App, command: InputCommand) -> Perhaps { - Ok(command.delegate(app, |command|Self::Input{command})?) - } - fn output (app: &mut App, command: OutputCommand) -> Perhaps { - Ok(command.delegate(app, |command|Self::Output{command})?) - } - fn clip (app: &mut App, command: ClipCommand) -> Perhaps { - Ok(command.delegate(app, |command|Self::Clip{command})?) + fn arrange (app: &mut App, command: ArrangementCommand) -> Perhaps { + Ok(command.delegate(app, |command|Self::Arrange{command})?) } fn clock (app: &mut App, command: ClockCommand) -> Perhaps { Ok(command.execute(&mut app.clock)?.map(|command|Self::Clock{command})) diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index ad036139..6cd4f34e 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -141,7 +141,7 @@ pub trait AutoRemove: } } } -} + /// Various possible dialog overlays #[derive(PartialEq, Clone, Copy, Debug)] @@ -180,6 +180,9 @@ has_editor!(|self: App|{ #[tengri_proc::expose] impl App { + fn _todo_u16_stub (&self) -> u16 { + todo!() + } fn _todo_isize_stub (&self) -> isize { todo!() } diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index ff8a7c07..d9d5fd0e 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -17,7 +17,7 @@ impl App { pub fn view_status (&self) -> impl Content + use<'_> { self.update_clock(); let cache = self.view_cache.read().unwrap(); - view_status(self.selected.describe(&self.tracks, &self.scenes), + view_status(self.selected.describe(self.tracks(), self.scenes()), cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone()) } pub fn view_transport (&self) -> impl Content + use<'_> { @@ -54,24 +54,24 @@ impl App { self.sampler().map(|s|s.view_meters_output()) } pub fn view_dialog (&self) -> impl Content + use<'_> { - When(app.dialog.is_some(), Bsp::b( "", + When(self.dialog.is_some(), Bsp::b( "", Fixed::xy(70, 23, Tui::fg_bg(Rgb(255,255,255), Rgb(16,16,16), Bsp::b( Repeat(" "), Outer(true, Style::default().fg(Tui::g(96))) - .enclose(app.dialog.as_ref().map(|dialog|match dialog { + .enclose(self.dialog.as_ref().map(|dialog|match dialog { Dialog::Menu => - app.view_dialog_menu().boxed(), + self.view_dialog_menu().boxed(), Dialog::Help => - app.view_dialog_help().boxed(), - Dialog::Save => - app.view_dialog_save().boxed(), - Dialog::Load => - app.view_dialog_load().boxed(), + self.view_dialog_help().boxed(), + Dialog::Save(browser) => + self.view_dialog_save().boxed(), + Dialog::Load(browser) => + self.view_dialog_load().boxed(), Dialog::Options => - app.view_dialog_options().boxed(), + self.view_dialog_options().boxed(), Dialog::Device(index) => - app.view_dialog_device(*index).boxed(), + self.view_dialog_device(*index).boxed(), Dialog::Message(message) => - app.view_dialog_message(message).boxed(), + self.view_dialog_message(message).boxed(), })) ))) )) diff --git a/crates/device/src/arranger/arranger_api.rs b/crates/device/src/arranger/arranger_api.rs index 6180a5d1..44d9fdd8 100644 --- a/crates/device/src/arranger/arranger_api.rs +++ b/crates/device/src/arranger/arranger_api.rs @@ -36,37 +36,37 @@ impl ArrangementCommand { Ok(None) } /// Set the color of the selected entity - fn set_color (arranger: &mut Arrangement, palette: Option) -> Option { + fn set_color (arranger: &mut Arrangement, palette: Option) -> Perhaps { use Selection::*; let palette = palette.unwrap_or_else(||ItemTheme::random()); - Some(match arranger.selected { + Ok(Some(Self::SetColor { palette: match arranger.selected { Some(Mix) => { let old = arranger.color; arranger.color = palette; - old + Some(old) }, Some(Scene(s)) => { let old = arranger.scenes[s].color; arranger.scenes[s].color = palette; - old + Some(old) } Some(Track(t)) => { let old = arranger.tracks[t].color; arranger.tracks[t].color = palette; - old + Some(old) } Some(TrackClip { track, scene }) => { if let Some(ref clip) = arranger.scenes[scene].clips[track] { let mut clip = clip.write().unwrap(); let old = clip.color; clip.color = palette; - old + Some(old) } else { - return None + None } }, _ => todo!() - }) + } })) } fn track (arranger: &mut Arrangement, track: TrackCommand) -> Perhaps { diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs index 37b8e593..0375819b 100644 --- a/crates/device/src/arranger/arranger_view.rs +++ b/crates/device/src/arranger/arranger_view.rs @@ -167,11 +167,7 @@ impl<'a> ArrangerView<'a> { .left(*width_side, Map::new( move||arrangement.scenes_with_sizes( - *is_editing, - Self::H_SCENE, - Self::H_EDITOR, - selected_track, - selected_scene, + *is_editing, Self::H_SCENE, Self::H_EDITOR, selected_track, selected_scene, ).map_while(|(s, scene, y1, y2)|if y2 as u16 > *scenes_height { None } else { From 4fe51b526763cbcd85053e133caa37fb55b6919c Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 14 May 2025 16:06:10 +0300 Subject: [PATCH 08/12] down to 48 ugly ones --- crates/app/src/api.rs | 24 ++--- crates/app/src/audio.rs | 8 +- crates/app/src/model.rs | 88 +++++++++---------- crates/app/src/view.rs | 33 ++++--- crates/device/src/arranger/arranger_model.rs | 28 +++--- crates/device/src/arranger/arranger_select.rs | 5 +- crates/device/src/arranger/arranger_view.rs | 2 +- crates/device/src/clock/clock_view.rs | 4 +- crates/device/src/editor/editor_model.rs | 13 +-- 9 files changed, 104 insertions(+), 101 deletions(-) diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index f2fe97f5..10bbefed 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -43,16 +43,16 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm todo!() } fn launch (app: &mut App) -> Perhaps { - app.launch(); + app.project.launch(); Ok(None) } fn select (app: &mut App, selection: Selection) -> Perhaps { - app.arranger.select(selection); + app.project.select(selection); if let Some(ref mut editor) = app.editor { - editor.set_clip(match app.arranger.selected { + editor.set_clip(match app.project.selected { Some(Selection::TrackClip { track, scene }) if let Some(Some(Some(clip))) = app - .arranger + .project .scenes.get(scene) .map(|s|s.clips.get(track)) => @@ -70,11 +70,11 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm // autoedit: load focused clip in editor. } fn stop_all (app: &mut App) -> Perhaps { - app.stop_all(); + app.tracks_stop_all(); Ok(None) } fn sampler (app: &mut App, command: SamplerCommand) -> Perhaps { - Ok(app.sampler_mut() + Ok(app.project.sampler_mut() .map(|s|command.delegate(s, |command|Self::Sampler{command})) .transpose()? .flatten()) @@ -83,7 +83,7 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm Ok(command.delegate(app, |command|Self::Arrange{command})?) } fn clock (app: &mut App, command: ClockCommand) -> Perhaps { - Ok(command.execute(&mut app.clock)?.map(|command|Self::Clock{command})) + Ok(command.execute(&mut app.clock())?.map(|command|Self::Clock{command})) } fn device (app: &mut App, command: DeviceCommand) -> Perhaps { Ok(command.delegate(app, |command|Self::Device{command})?) @@ -95,7 +95,7 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm Ok(if let Some(editor) = app.editor.as_mut() { let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; // update linked sampler after editor action - app.sampler_mut().map(|sampler|match command { + app.project.sampler_mut().map(|sampler|match command { // autoselect: automatically select sample in sampler MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, _ => {} @@ -106,7 +106,7 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm }) } fn pool (app: &mut App, command: PoolCommand) -> Perhaps { - Ok(if let Some(pool) = app.pool.as_mut() { + Ok(if let Some(pool) = app.project.pool.as_mut() { let undo = command.clone().delegate(pool, |command|AppCommand::Pool{command})?; // update linked editor after pool action app.editor.as_mut().map(|editor|match command { @@ -126,7 +126,7 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm impl<'state> Context<'state, ClockCommand> for App { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { - Context::get(&self.clock, iter) + Context::get(&self.clock(), iter) } } @@ -138,13 +138,13 @@ impl<'state> Context<'state, MidiEditCommand> for App { impl<'state> Context<'state, PoolCommand> for App { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { - self.pool().map(|p|Context::get(p, iter)).flatten() + self.project.pool.map(|p|Context::get(p, iter)).flatten() } } impl<'state> Context<'state, SamplerCommand> for App { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { - self.sampler().map(|p|Context::get(p, iter)).flatten() + self.project.sampler().map(|p|Context::get(p, iter)).flatten() } } diff --git a/crates/app/src/audio.rs b/crates/app/src/audio.rs index 15d15232..e12c2b80 100644 --- a/crates/app/src/audio.rs +++ b/crates/app/src/audio.rs @@ -10,7 +10,7 @@ audio!( |self: App, client, scope|{ let t0 = self.perf.get_t0(); self.clock().update_from_scope(scope).unwrap(); - let midi_in = self.arranger.midi_input_collect(scope); + let midi_in = self.project.midi_input_collect(scope); if let Some(editor) = &self.editor { let mut pitch: Option = None; for port in midi_in.iter() { @@ -26,14 +26,16 @@ audio!( editor.set_note_pos(pitch.as_int() as usize); } } - let result = self.arranger.tracks_render(client, scope); + let result = self.project.process_tracks(client, scope); self.perf.update_from_jack_scope(t0, scope); result }; |self, event|{ use JackEvent::*; match event { - SampleRate(sr) => { self.clock.timebase.sr.set(sr as f64); }, + SampleRate(sr) => { + self.clock().timebase.sr.set(sr as f64); + }, PortRegistration(id, true) => { //let port = self.jack().port_by_id(id); //println!("\rport add: {id} {port:?}"); diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index 6cd4f34e..24cdcb4b 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -28,12 +28,26 @@ pub struct App { pub color: ItemTheme, } -has!(Option: |self: App|self.project.selected); +has!(Option: |self: App|self.project.selection); has!(Vec: |self: App|self.project.midi_ins); has!(Vec: |self: App|self.project.midi_outs); has!(Vec: |self: App|self.project.scenes); has!(Vec: |self: App|self.project.tracks); has!(Clock: |self: App|self.project.clock); +has_size!(|self: App|&self.size); +has_clips!(|self: App|self.project.pool.clips); +has_editor!(|self: App|{ + editor = self.editor; + editor_w = { + let size = self.size.w(); + let editor = self.editor.as_ref().expect("missing editor"); + let time_len = editor.time_len().get(); + let time_zoom = editor.time_zoom().get().max(1); + (5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) + }; + editor_h = 15; + is_editing = self.editing.load(Relaxed); +}); impl App { pub fn toggle_dialog (&mut self, dialog: Option) { @@ -92,14 +106,13 @@ impl App { } // Create new clip in pool when entering empty cell fn clip_auto_create (&mut self) -> Option>> { - if let Some(pool) = Has::>::get(self) - && let Some(Selection::TrackClip { track, scene }) = self.get() - && let Some(scene) = Has::>::get_mut(self).get_mut(*scene) - && let Some(slot) = scene.clips.get_mut(*track) - && slot.is_none() - && let Some(track) = Has::>::get_mut(self).get_mut(*track) + if let Some(Selection::TrackClip { track, scene }) = self.get() + && let Some(scene) = Has::>::get_mut(self).get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && slot.is_none() + && let Some(track) = Has::>::get_mut(self).get_mut(track) { - let (index, mut clip) = pool.add_new_clip(); + let (index, mut clip) = self.arranger.pool.add_new_clip(); // autocolor: new clip colors from scene and track color let color = track.color.base.mix(scene.color.base, 0.5); clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); @@ -144,7 +157,7 @@ pub trait AutoRemove: /// Various possible dialog overlays -#[derive(PartialEq, Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub enum Dialog { Help, Menu, @@ -162,21 +175,6 @@ pub enum Message { } content!(TuiOut: |self: Message| match self { Self::FailedToAddDevice => "Failed to add device." }); -has_size!(|self: App|&self.size); -has!(Clock: |self: App|self.clock); -has_clips!(|self: App|self.pool.as_ref().expect("no clip pool").clips); -has_editor!(|self: App|{ - editor = self.editor; - editor_w = { - let size = self.size.w(); - let editor = self.editor.as_ref().expect("missing editor"); - let time_len = editor.time_len().get(); - let time_zoom = editor.time_zoom().get().max(1); - (5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) - }; - editor_h = 15; - is_editing = self.editing.load(Relaxed); -}); #[tengri_proc::expose] impl App { @@ -202,28 +200,28 @@ impl App { self.browser.is_visible } fn focus_clip (&self) -> bool { - !self.is_editing() && self.selected.is_clip() + !self.is_editing() && self.selection().is_clip() } fn focus_track (&self) -> bool { - !self.is_editing() && self.selected.is_track() + !self.is_editing() && self.selection().is_track() } fn focus_scene (&self) -> bool { - !self.is_editing() && self.selected.is_scene() + !self.is_editing() && self.selection().is_scene() } fn focus_mix (&self) -> bool { - !self.is_editing() && self.selected.is_mix() + !self.is_editing() && self.selection().is_mix() } fn focus_pool_import (&self) -> bool { - matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Import(..))) + matches!(self.project.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Import(..))) } fn focus_pool_export (&self) -> bool { - matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Export(..))) + matches!(self.project.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Export(..))) } fn focus_pool_rename (&self) -> bool { - matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Rename(..))) + matches!(self.project.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Rename(..))) } fn focus_pool_length (&self) -> bool { - matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Length(..))) + matches!(self.project.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Length(..))) } fn dialog_device (&self) -> Dialog { Dialog::Device(0) // TODO @@ -250,38 +248,38 @@ impl App { Dialog::Options } fn editor_pitch (&self) -> Option { - Some((self.editor().map(|e|e.get_note_pos()).unwrap() as u8).into()) + Some((self.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) } fn scene_count (&self) -> usize { self.scenes.len() } - fn scene_selected (&self) -> Option { - self.selected.scene() + fn scene_selection (&self) -> Option { + self.selection().scene() } fn track_count (&self) -> usize { self.tracks.len() } - fn track_selected (&self) -> Option { - self.selected.track() + fn track_selection (&self) -> Option { + self.selection().track() } fn select_scene_next (&self) -> Selection { - self.selected.scene_next(self.scenes.len()) + self.selection().scene_next(self.scenes().len()) } fn select_scene_prev (&self) -> Selection { - self.selected.scene_prev() + self.selection().scene_prev() } fn select_track_header (&self) -> Selection { - self.selected.track_header(self.tracks.len()) + self.selection().track_header(self.tracks().len()) } fn select_track_next (&self) -> Selection { - self.selected.track_next(self.tracks.len()) + self.selection().track_next(self.tracks().len()) } fn select_track_prev (&self) -> Selection { - self.selected.track_prev() + self.selection().track_prev() } - fn clip_selected (&self) -> Option>> { - match self.selected { - Selection::TrackClip { track, scene } => self.scenes[scene].clips[track].clone(), + fn clip_selection (&self) -> Option>> { + match self.selection() { + Selection::TrackClip { track, scene } => self.scenes()[scene].clips[track].clone(), _ => None } } diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index d9d5fd0e..86886df1 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -17,41 +17,43 @@ impl App { pub fn view_status (&self) -> impl Content + use<'_> { self.update_clock(); let cache = self.view_cache.read().unwrap(); - view_status(self.selected.describe(self.tracks(), self.scenes()), + view_status(self.project.selected.map(|x|x.describe(self.tracks(), self.scenes())), cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone()) } pub fn view_transport (&self) -> impl Content + use<'_> { self.update_clock(); let cache = self.view_cache.read().unwrap(); - view_transport(self.clock.is_rolling(), + view_transport(self.project.clock.is_rolling(), cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone()) } - pub fn view_arranger (&self) -> impl Content + use<'_> { - ArrangerView::new(self, self.editor.as_ref()) - } - pub fn view_pool (&self) -> impl Content + use<'_> { - self.pool().map(|p|Fixed::x(self.w_sidebar(), PoolView(self.is_editing(), p))) - } pub fn view_editor (&self) -> impl Content + use<'_> { self.editor().map(|e|Bsp::n(Bsp::e(e.clip_status(), e.edit_status()), e)) } + pub fn view_arranger (&self) -> impl Content + use<'_> { + ArrangerView::new(&self.project, self.editor.as_ref()) + } + pub fn view_pool (&self) -> impl Content + use<'_> { + let is_editing = self.is_editing(); + Fixed::x(self.project.w_sidebar(is_editing), + PoolView(is_editing, &self.project.pool)) + } pub fn view_samples_keys (&self) -> impl Content + use<'_> { - self.sampler().map(|s|s.view_list(false, self.editor().unwrap())) + self.project.sampler().map(|s|s.view_list(false, self.editor().unwrap())) } pub fn view_samples_grid (&self) -> impl Content + use<'_> { - self.sampler().map(|s|s.view_grid()) + self.project.sampler().map(|s|s.view_grid()) } pub fn view_sample_viewer (&self) -> impl Content + use<'_> { - self.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) + self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) } pub fn view_sample_info (&self) -> impl Content + use<'_> { - self.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) + self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) } pub fn view_meters_input (&self) -> impl Content + use<'_> { - self.sampler().map(|s|s.view_meters_input()) + self.project.sampler().map(|s|s.view_meters_input()) } pub fn view_meters_output (&self) -> impl Content + use<'_> { - self.sampler().map(|s|s.view_meters_output()) + self.project.sampler().map(|s|s.view_meters_output()) } pub fn view_dialog (&self) -> impl Content + use<'_> { When(self.dialog.is_some(), Bsp::b( "", @@ -76,6 +78,9 @@ impl App { ))) )) } +} + +impl App { pub fn view_dialog_menu (&self) -> impl Content { let options = ||["Projects", "Settings", "Help", "Quit"].iter(); let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); diff --git a/crates/device/src/arranger/arranger_model.rs b/crates/device/src/arranger/arranger_model.rs index 7d59ed67..2fa8556c 100644 --- a/crates/device/src/arranger/arranger_model.rs +++ b/crates/device/src/arranger/arranger_model.rs @@ -53,33 +53,33 @@ has!(Clock: |self: Arrangement|self.clock); impl Arrangement { /// Width of display - pub(crate) fn w (&self) -> u16 { + pub fn w (&self) -> u16 { self.size.w() as u16 } /// Width allocated for sidebar. - pub(crate) fn w_sidebar (&self, is_editing: bool) -> u16 { + pub fn w_sidebar (&self, is_editing: bool) -> u16 { self.w() / if is_editing { 16 } else { 8 } as u16 } /// Width taken by all tracks. - pub(crate) fn w_tracks (&self) -> u16 { + pub fn w_tracks (&self) -> u16 { self.tracks_with_sizes(&self.selected, None).last() .map(|(_, _, _, x)|x as u16).unwrap_or(0) } /// Width available to display tracks. - pub(crate) fn w_tracks_area (&self, is_editing: bool) -> u16 { + pub fn w_tracks_area (&self, is_editing: bool) -> u16 { self.w().saturating_sub(2 * self.w_sidebar(is_editing)) } /// Height of display - pub(crate) fn h (&self) -> u16 { + pub fn h (&self) -> u16 { self.size.h() as u16 } /// Height available to display track headers. - pub(crate) fn h_tracks_area (&self) -> u16 { + pub fn h_tracks_area (&self) -> u16 { 5 // FIXME //self.h().saturating_sub(self.h_inputs() + self.h_outputs()) } /// Height available to display tracks. - pub(crate) fn h_scenes_area (&self) -> u16 { + pub fn h_scenes_area (&self) -> u16 { //15 self.h().saturating_sub( self.h_inputs() + @@ -89,7 +89,7 @@ impl Arrangement { ) } /// Height taken by all scenes. - pub(crate) fn h_scenes (&self, is_editing: bool) -> u16 { + pub fn h_scenes (&self, is_editing: bool) -> u16 { let (selected_track, selected_scene) = match Has::>::get(self) { Some(Selection::Track(t)) => (Some(*t), None), Some(Selection::Scene(s)) => (None, Some(*s)), @@ -107,15 +107,15 @@ impl Arrangement { .map(|(_, _, _, y)|y as u16).unwrap_or(0) } /// Height taken by all inputs. - pub(crate) fn h_inputs (&self) -> u16 { + pub fn h_inputs (&self) -> u16 { self.midi_ins_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } /// Height taken by all outputs. - pub(crate) fn h_outputs (&self) -> u16 { + pub fn h_outputs (&self) -> u16 { self.midi_outs_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } /// Height taken by visible device slots. - pub(crate) fn h_devices (&self) -> u16 { + pub fn h_devices (&self) -> u16 { 2 //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } @@ -144,7 +144,7 @@ impl Arrangement { self.get_scene()?.clips.get(self.selection()?.track()?)?.clone() } /// Put a clip in a slot - pub(crate) fn clip_put ( + pub fn clip_put ( &mut self, track: usize, scene: usize, clip: Option>> ) -> Option>> { let old = self.scenes[scene].clips[track].clone(); @@ -152,7 +152,7 @@ impl Arrangement { old } /// Change the color of a clip, returning the previous one - pub(crate) fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) + pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) -> Option { self.scenes[scene].clips[track].as_ref().map(|clip|{ @@ -164,7 +164,7 @@ impl Arrangement { }) } /// Toggle looping for the active clip - pub(crate) fn toggle_loop (&mut self) { + pub fn toggle_loop (&mut self) { if let Some(clip) = self.get_clip() { clip.write().unwrap().toggle_loop() } diff --git a/crates/device/src/arranger/arranger_select.rs b/crates/device/src/arranger/arranger_select.rs index 81a2f7a1..856e53eb 100644 --- a/crates/device/src/arranger/arranger_select.rs +++ b/crates/device/src/arranger/arranger_select.rs @@ -17,8 +17,11 @@ impl Arrangement { /// 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 - #[default] Mix, + Mix, /// A MIDI input is selected. Input(usize), /// A MIDI output is selected. diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs index 0375819b..b05905af 100644 --- a/crates/device/src/arranger/arranger_view.rs +++ b/crates/device/src/arranger/arranger_view.rs @@ -1,6 +1,6 @@ use crate::*; -pub(crate) struct ArrangerView<'a> { +pub struct ArrangerView<'a> { pub arrangement: &'a Arrangement, pub is_editing: bool, diff --git a/crates/device/src/clock/clock_view.rs b/crates/device/src/clock/clock_view.rs index ab2acc3e..92e778eb 100644 --- a/crates/device/src/clock/clock_view.rs +++ b/crates/device/src/clock/clock_view.rs @@ -18,14 +18,14 @@ pub fn view_transport ( } pub fn view_status ( - sel: Arc, + sel: Option>, sr: Arc>, buf: Arc>, lat: Arc>, ) -> impl Content { let theme = ItemTheme::G[96]; Tui::bg(Black, row!(Bsp::a( - Fill::xy(Align::w(FieldH(theme, "Selected", sel))), + Fill::xy(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))), Fill::xy(Align::e(row!( FieldH(theme, "SR", sr), FieldH(theme, "Buf", buf), diff --git a/crates/device/src/editor/editor_model.rs b/crates/device/src/editor/editor_model.rs index 648e70c5..9d8d31f6 100644 --- a/crates/device/src/editor/editor_model.rs +++ b/crates/device/src/editor/editor_model.rs @@ -102,8 +102,8 @@ impl MidiViewer for MidiEditor { } pub trait HasEditor { - fn editor (&self) -> &Option; - fn editor_mut (&mut self) -> &Option; + fn editor (&self) -> Option<&MidiEditor>; + fn editor_mut (&mut self) -> Option<&mut MidiEditor>; fn is_editing (&self) -> bool { true } fn editor_w (&self) -> usize { 0 } fn editor_h (&self) -> usize { 0 } @@ -117,17 +117,12 @@ pub trait HasEditor { is_editing = $e3:expr; }) => { impl HasEditor for $Struct { - fn editor (&$self) -> &Option { &$e0 } - fn editor_mut (&mut $self) -> &Option { &mut $e0 } + fn editor (&$self) -> Option<&MidiEditor> { $e0.as_ref() } + fn editor_mut (&mut $self) -> Option<&mut MidiEditor> { $e0.as_mut() } fn editor_w (&$self) -> usize { $e1 } fn editor_h (&$self) -> usize { $e2 } fn is_editing (&$self) -> bool { $e3 } } }; - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? { - fn editor (&$self) -> &MidiEditor { &$cb } - } - }; } From 872c2d94d6f5603c8881137c640c0069e16627f0 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 14 May 2025 17:15:27 +0300 Subject: [PATCH 09/12] last small wave of 15 errors? --- crates/app/src/api.rs | 73 +++++------ crates/app/src/audio.rs | 6 - crates/app/src/model.rs | 113 ++++++++---------- crates/app/src/view.rs | 2 +- crates/device/src/arranger/arranger_api.rs | 81 ++++++------- crates/device/src/arranger/arranger_model.rs | 38 +++--- crates/device/src/arranger/arranger_select.rs | 13 +- crates/device/src/arranger/arranger_track.rs | 22 +++- crates/device/src/arranger/arranger_tracks.rs | 9 +- crates/device/src/arranger/arranger_view.rs | 11 +- crates/device/src/browser/browser_model.rs | 2 +- 11 files changed, 181 insertions(+), 189 deletions(-) diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index 10bbefed..90b086a0 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -30,9 +30,9 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm app.toggle_editor(Some(value)); Ok(None) } - fn color (app: &mut App, theme: ItemTheme) -> Perhaps { - Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) - } + //fn color (app: &mut App, theme: ItemTheme) -> Perhaps { + //Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) + //} fn enqueue (app: &mut App, clip: Option>>) -> Perhaps { todo!() } @@ -42,19 +42,18 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm fn zoom (app: &mut App, zoom: usize) -> Perhaps { todo!() } - fn launch (app: &mut App) -> Perhaps { - app.project.launch(); - Ok(None) - } + //fn launch (app: &mut App) -> Perhaps { + //app.project.launch(); + //Ok(None) + //} fn select (app: &mut App, selection: Selection) -> Perhaps { - app.project.select(selection); + *app.project.selection_mut() = selection; if let Some(ref mut editor) = app.editor { - editor.set_clip(match app.project.selected { - Some(Selection::TrackClip { track, scene }) - if let Some(Some(Some(clip))) = app + editor.set_clip(match app.project.selection() { + Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app .project - .scenes.get(scene) - .map(|s|s.clips.get(track)) + .scenes.get(*scene) + .map(|s|s.clips.get(*track)) => Some(clip), _ => @@ -79,14 +78,11 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm .transpose()? .flatten()) } - fn arrange (app: &mut App, command: ArrangementCommand) -> Perhaps { - Ok(command.delegate(app, |command|Self::Arrange{command})?) + fn project (app: &mut App, command: ArrangementCommand) -> Perhaps { + Ok(command.delegate(&mut app.project, |command|Self::Project{command})?) } fn clock (app: &mut App, command: ClockCommand) -> Perhaps { - Ok(command.execute(&mut app.clock())?.map(|command|Self::Clock{command})) - } - fn device (app: &mut App, command: DeviceCommand) -> Perhaps { - Ok(command.delegate(app, |command|Self::Device{command})?) + Ok(command.execute(app.clock_mut())?.map(|command|Self::Clock{command})) } fn message (app: &mut App, command: MessageCommand) -> Perhaps { Ok(command.delegate(app, |command|Self::Message{command})?) @@ -106,21 +102,20 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm }) } fn pool (app: &mut App, command: PoolCommand) -> Perhaps { - Ok(if let Some(pool) = app.project.pool.as_mut() { - let undo = command.clone().delegate(pool, |command|AppCommand::Pool{command})?; - // update linked editor after pool action - app.editor.as_mut().map(|editor|match command { - // autoselect: automatically load selected clip in editor - PoolCommand::Select { .. } | - // autocolor: update color in all places simultaneously - PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => - editor.set_clip(pool.clip().as_ref()), - _ => {} - }); - undo - } else { - None - }) + let undo = command.clone().delegate( + &mut app.project.pool, + |command|AppCommand::Pool{command} + )?; + // update linked editor after pool action + app.editor.as_mut().map(|editor|match command { + // autoselect: automatically load selected clip in editor + PoolCommand::Select { .. } | + // autocolor: update color in all places simultaneously + PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => + editor.set_clip(app.project.pool.clip().as_ref()), + _ => {} + }); + Ok(undo) } } @@ -138,7 +133,7 @@ impl<'state> Context<'state, MidiEditCommand> for App { impl<'state> Context<'state, PoolCommand> for App { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { - self.project.pool.map(|p|Context::get(p, iter)).flatten() + Context::get(&self.project.pool, iter) } } @@ -148,9 +143,15 @@ impl<'state> Context<'state, SamplerCommand> for App { } } +impl<'state> Context<'state, ArrangementCommand> for App { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { + Context::get(&self.project, iter) + } +} + #[tengri_proc::command(App)] impl MessageCommand { fn dismiss (app: &mut App) -> Perhaps { - app.message_dismiss(); + app.dialog = None; Ok(None) } } diff --git a/crates/app/src/audio.rs b/crates/app/src/audio.rs index e12c2b80..dd65d0af 100644 --- a/crates/app/src/audio.rs +++ b/crates/app/src/audio.rs @@ -1,11 +1,5 @@ use crate::*; -impl HasJack for App { - fn jack (&self) -> &Jack { - &self.jack - } -} - audio!( |self: App, client, scope|{ let t0 = self.perf.get_t0(); diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index 24cdcb4b..c04f1fb3 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -28,12 +28,14 @@ pub struct App { pub color: ItemTheme, } -has!(Option: |self: App|self.project.selection); -has!(Vec: |self: App|self.project.midi_ins); -has!(Vec: |self: App|self.project.midi_outs); -has!(Vec: |self: App|self.project.scenes); -has!(Vec: |self: App|self.project.tracks); -has!(Clock: |self: App|self.project.clock); +has!(Jack: |self: App|self.jack); +has!(Clock: |self: App|self.project.clock); +has!(Selection: |self: App|self.project.selection); +has!(Vec: |self: App|self.project.midi_ins); +has!(Vec: |self: App|self.project.midi_outs); +has!(Vec: |self: App|self.project.scenes); +has!(Vec: |self: App|self.project.tracks); + has_size!(|self: App|&self.size); has_clips!(|self: App|self.project.pool.clips); has_editor!(|self: App|{ @@ -46,24 +48,28 @@ has_editor!(|self: App|{ (5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) }; editor_h = 15; - is_editing = self.editing.load(Relaxed); + is_editing = self.editor.is_some(); }); impl App { - pub fn toggle_dialog (&mut self, dialog: Option) { - self.dialog = if self.dialog == dialog { - None - } else { - dialog - } + pub fn toggle_dialog (&mut self, mut dialog: Option) -> Option { + std::mem::swap(&mut self.dialog, &mut dialog); + dialog } pub fn toggle_editor (&mut self, value: Option) { - self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); - self.arranger.map(|arranger|if value { - arranger.clip_auto_create(); + //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); + let value = value.unwrap_or_else(||!self.editor.is_some()); + if value { + self.clip_auto_create(); } else { - arranger.clip_auto_remove(); - }); + self.clip_auto_remove(); + } + } + pub fn browser (&self) -> Option<&Browser> { + self.dialog.as_ref().and_then(|dialog|match dialog { + Dialog::Save(b) | Dialog::Load(b) => Some(b), + _ => None + }) } pub(crate) fn device_pick (&mut self, index: usize) { self.dialog = Some(Dialog::Device(index)); @@ -85,15 +91,14 @@ impl App { todo!(); Ok(()) } - fn device_add_sampler (&mut self, jack: &Jack) -> Usually { - let name = jack.with_client(|c|c.name().to_string()); - let midi = self.track().expect("no active track").sequencer.midi_outs[0].name(); + fn device_add_sampler (&mut self) -> Usually<()> { + let name = self.jack.with_client(|c|c.name().to_string()); + let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].name(); + let track = self.track().expect("no active track"); + let port = format!("{}/Sampler", &track.name); + let connect = PortConnect::exact(format!("{name}:{midi}")); let sampler = if let Ok(sampler) = Sampler::new( - jack, - &format!("{}/Sampler", Has::::get(self).name), - &[PortConnect::exact(format!("{name}:{midi}"))], - &[&[], &[]], - &[&[], &[]] + &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] ) { self.dialog = None; Device::Sampler(sampler) @@ -101,22 +106,23 @@ impl App { self.dialog = Some(Dialog::Message(Message::FailedToAddDevice)); return Err("failed to add device".into()) }; - Has::::get_mut(self).expect("no active track").devices.push(sampler); + let track = self.track_mut().expect("no active track"); + track.devices.push(sampler); Ok(()) } // Create new clip in pool when entering empty cell fn clip_auto_create (&mut self) -> Option>> { - if let Some(Selection::TrackClip { track, scene }) = self.get() - && let Some(scene) = Has::>::get_mut(self).get_mut(scene) + if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) && let Some(slot) = scene.clips.get_mut(track) && slot.is_none() - && let Some(track) = Has::>::get_mut(self).get_mut(track) + && let Some(track) = self.project.tracks.get_mut(track) { - let (index, mut clip) = self.arranger.pool.add_new_clip(); + let (index, mut clip) = self.project.pool.add_new_clip(); // autocolor: new clip colors from scene and track color let color = track.color.base.mix(scene.color.base, 0.5); clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); - if let Some(ref mut editor) = Has::>::get_mut(self) { + if let Some(ref mut editor) = self.editor { editor.set_clip(Some(&clip)); } *slot = Some(clip.clone()); @@ -125,37 +131,24 @@ impl App { None } } -} - -#[cfg(all(feature = "pool", feature = "editor"))] -pub trait AutoRemove: - Has> + - Has> + - Has> + - Has> + - Has> + - Send + Sync -{ // Remove clip from arrangement when exiting empty clip editor fn clip_auto_remove (&mut self) { - if let Some(ref mut pool) = Has::>::get(self) - && let Some(Selection::TrackClip { track, scene }) = self.get() - && let Some(scene) = Has::>::get_mut(self).get_mut(*scene) - && let Some(slot) = scene.clips.get_mut(*track) - && let Some(clip) = slot.as_mut() + if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && let Some(clip) = slot.as_mut() { let mut swapped = None; if clip.read().unwrap().count_midi_messages() == 0 { std::mem::swap(&mut swapped, slot); } if let Some(clip) = swapped { - pool.delete_clip(&clip.read().unwrap()); + self.project.pool.delete_clip(&clip.read().unwrap()); } } } } - /// Various possible dialog overlays #[derive(Clone, Debug)] pub enum Dialog { @@ -197,7 +190,7 @@ impl App { matches!(self.dialog, Some(Dialog::Device(..))) } fn focus_browser (&self) -> bool { - self.browser.is_visible + self.browser().is_some() } fn focus_clip (&self) -> bool { !self.is_editing() && self.selection().is_clip() @@ -212,16 +205,16 @@ impl App { !self.is_editing() && self.selection().is_mix() } fn focus_pool_import (&self) -> bool { - matches!(self.project.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Import(..))) + matches!(self.project.pool.mode, Some(PoolMode::Import(..))) } fn focus_pool_export (&self) -> bool { - matches!(self.project.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Export(..))) + matches!(self.project.pool.mode, Some(PoolMode::Export(..))) } fn focus_pool_rename (&self) -> bool { - matches!(self.project.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Rename(..))) + matches!(self.project.pool.mode, Some(PoolMode::Rename(..))) } fn focus_pool_length (&self) -> bool { - matches!(self.project.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Length(..))) + matches!(self.project.pool.mode, Some(PoolMode::Length(..))) } fn dialog_device (&self) -> Dialog { Dialog::Device(0) // TODO @@ -239,10 +232,10 @@ impl App { Dialog::Menu } fn dialog_save (&self) -> Dialog { - Dialog::Save + Dialog::Save(Default::default()) } fn dialog_load (&self) -> Dialog { - Dialog::Load + Dialog::Load(Default::default()) } fn dialog_options (&self) -> Dialog { Dialog::Options @@ -251,13 +244,13 @@ impl App { Some((self.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) } fn scene_count (&self) -> usize { - self.scenes.len() + self.scenes().len() } fn scene_selection (&self) -> Option { self.selection().scene() } fn track_count (&self) -> usize { - self.tracks.len() + self.tracks().len() } fn track_selection (&self) -> Option { self.selection().track() @@ -279,7 +272,7 @@ impl App { } fn clip_selection (&self) -> Option>> { match self.selection() { - Selection::TrackClip { track, scene } => self.scenes()[scene].clips[track].clone(), + Selection::TrackClip { track, scene } => self.scenes()[*scene].clips[*track].clone(), _ => None } } diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index 86886df1..55980e5e 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -17,7 +17,7 @@ impl App { pub fn view_status (&self) -> impl Content + use<'_> { self.update_clock(); let cache = self.view_cache.read().unwrap(); - view_status(self.project.selected.map(|x|x.describe(self.tracks(), self.scenes())), + view_status(Some(self.project.selection.describe(self.tracks(), self.scenes())), cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone()) } pub fn view_transport (&self) -> impl Content + use<'_> { diff --git a/crates/device/src/arranger/arranger_api.rs b/crates/device/src/arranger/arranger_api.rs index 44d9fdd8..daa31c67 100644 --- a/crates/device/src/arranger/arranger_api.rs +++ b/crates/device/src/arranger/arranger_api.rs @@ -6,27 +6,28 @@ impl Arrangement { fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } - fn _todo_opt_selection_ (&self) -> Option { todo!() } + fn select_nothing (&self) -> Selection { + Selection::Nothing + } } #[tengri_proc::command(Arrangement)] impl ArrangementCommand { /// Set the selection - fn select (arranger: &mut Arrangement, s: Option) -> Perhaps { - arranger.selected = s; + fn select (arranger: &mut Arrangement, s: Selection) -> Perhaps { + *arranger.selection_mut() = s; Ok(None) } /// Launch a clip or scene fn launch (arranger: &mut Arrangement) -> Perhaps { - use Selection::*; - match arranger.selected { - Some(Track(t)) => { + match *arranger.selection() { + Selection::Track(t) => { arranger.tracks[t].sequencer.enqueue_next(None) }, - Some(TrackClip { track, scene }) => { + Selection::TrackClip { track, scene } => { arranger.tracks[track].sequencer.enqueue_next(arranger.scenes[scene].clips[track].as_ref()) }, - Some(Scene(s)) => { + Selection::Scene(s) => { for t in 0..arranger.tracks.len() { arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref()) } @@ -37,49 +38,44 @@ impl ArrangementCommand { } /// Set the color of the selected entity fn set_color (arranger: &mut Arrangement, palette: Option) -> Perhaps { - use Selection::*; - let palette = palette.unwrap_or_else(||ItemTheme::random()); - Ok(Some(Self::SetColor { palette: match arranger.selected { - Some(Mix) => { - let old = arranger.color; - arranger.color = palette; - Some(old) + let mut palette = palette.unwrap_or_else(||ItemTheme::random()); + let selection = *arranger.selection(); + Ok(Some(Self::SetColor { palette: Some(match selection { + Selection::Mix => { + std::mem::swap(&mut palette, &mut arranger.color); + palette }, - Some(Scene(s)) => { - let old = arranger.scenes[s].color; - arranger.scenes[s].color = palette; - Some(old) + Selection::Scene(s) => { + std::mem::swap(&mut palette, &mut arranger.scenes[s].color); + palette } - Some(Track(t)) => { - let old = arranger.tracks[t].color; - arranger.tracks[t].color = palette; - Some(old) + Selection::Track(t) => { + std::mem::swap(&mut palette, &mut arranger.tracks[t].color); + palette } - Some(TrackClip { track, scene }) => { + Selection::TrackClip { track, scene } => { if let Some(ref clip) = arranger.scenes[scene].clips[track] { let mut clip = clip.write().unwrap(); - let old = clip.color; - clip.color = palette; - Some(old) + std::mem::swap(&mut palette, &mut clip.color); + palette } else { - None + return Ok(None) } }, _ => todo!() - } })) + }) })) } - fn track (arranger: &mut Arrangement, track: TrackCommand) -> Perhaps { todo!("delegate") } fn track_add (arranger: &mut Arrangement) -> Perhaps { let index = arranger.track_add(None, None, &[], &[])?.0; - arranger.selected = match arranger.selected { - Some(Selection::Track(_)) => - Some(Selection::Track(index)), - Some(Selection::TrackClip { track, scene }) => - Some(Selection::TrackClip { track: index, scene }), - _ => arranger.selected + *arranger.selection_mut() = match arranger.selection() { + Selection::Track(_) => Selection::Track(index), + Selection::TrackClip { track, scene } => Selection::TrackClip { + track: index, scene: *scene + }, + _ => *arranger.selection() }; Ok(Some(Self::TrackDelete { index })) } @@ -135,12 +131,13 @@ impl ArrangementCommand { } fn scene_add (arranger: &mut Arrangement) -> Perhaps { let index = arranger.scene_add(None, None)?.0; - arranger.selected = match arranger.selected { - Some(Selection::Scene(_)) => - Some(Selection::Scene(index)), - Some(Selection::TrackClip { track, scene }) => - Some(Selection::TrackClip { track, scene: index }), - _ => arranger.selected + *arranger.selection_mut() = match arranger.selection() { + Selection::Scene(_) => Selection::Scene(index), + Selection::TrackClip { track, scene } => Selection::TrackClip { + track: *track, + scene: index + }, + _ => *arranger.selection() }; Ok(None) // TODO } diff --git a/crates/device/src/arranger/arranger_model.rs b/crates/device/src/arranger/arranger_model.rs index 2fa8556c..a1d680fc 100644 --- a/crates/device/src/arranger/arranger_model.rs +++ b/crates/device/src/arranger/arranger_model.rs @@ -33,7 +33,7 @@ pub struct Arrangement { /// Scroll offset of scenes pub scene_scroll: usize, /// Selected UI element - pub selected: Option, + pub selection: Selection, /// Contains a render of the project arrangement, redrawn on update. /// TODO rename to "render_cache" or smth pub arranger: Arc>, @@ -43,13 +43,13 @@ pub struct Arrangement { pub pool: Pool, } -has!(Option: |self: Arrangement|self.selected); -has!(Vec: |self: Arrangement|self.midi_ins); -has!(Vec: |self: Arrangement|self.midi_outs); -has!(Vec: |self: Arrangement|self.scenes); -has!(Vec: |self: Arrangement|self.tracks); -has!(Jack: |self: Arrangement|self.jack); -has!(Clock: |self: Arrangement|self.clock); +has!(Jack: |self: Arrangement|self.jack); +has!(Clock: |self: Arrangement|self.clock); +has!(Selection: |self: Arrangement|self.selection); +has!(Vec: |self: Arrangement|self.midi_ins); +has!(Vec: |self: Arrangement|self.midi_outs); +has!(Vec: |self: Arrangement|self.scenes); +has!(Vec: |self: Arrangement|self.tracks); impl Arrangement { /// Width of display @@ -62,7 +62,7 @@ impl Arrangement { } /// Width taken by all tracks. pub fn w_tracks (&self) -> u16 { - self.tracks_with_sizes(&self.selected, None).last() + self.tracks_with_sizes(&self.selection(), None).last() .map(|(_, _, _, x)|x as u16).unwrap_or(0) } /// Width available to display tracks. @@ -90,18 +90,12 @@ impl Arrangement { } /// Height taken by all scenes. pub fn h_scenes (&self, is_editing: bool) -> u16 { - let (selected_track, selected_scene) = match Has::>::get(self) { - Some(Selection::Track(t)) => (Some(*t), None), - Some(Selection::Scene(s)) => (None, Some(*s)), - Some(Selection::TrackClip { track, scene }) => (Some(*track), Some(*scene)), - _ => (None, None) - }; self.scenes_with_sizes( is_editing, ArrangerView::H_SCENE, ArrangerView::H_EDITOR, - selected_track, - selected_scene + self.selection().track(), + self.selection().scene(), ) .last() .map(|(_, _, _, y)|y as u16).unwrap_or(0) @@ -121,27 +115,27 @@ impl Arrangement { } /// Get the active track fn get_track (&self) -> Option<&Track> { - let index = self.selection()?.track()?; + let index = self.selection().track()?; Has::>::get(self).get(index) } /// Get a mutable reference to the active track fn get_track_mut (&mut self) -> Option<&mut Track> { - let index = self.selection()?.track()?; + let index = self.selection().track()?; Has::>::get_mut(self).get_mut(index) } /// Get the active scene fn get_scene (&self) -> Option<&Scene> { - let index = self.selection()?.scene()?; + let index = self.selection().scene()?; Has::>::get(self).get(index) } /// Get a mutable reference to the active scene fn get_scene_mut (&mut self) -> Option<&mut Scene> { - let index = self.selection()?.scene()?; + let index = self.selection().scene()?; Has::>::get_mut(self).get_mut(index) } /// Get the active clip fn get_clip (&self) -> Option>> { - self.get_scene()?.clips.get(self.selection()?.track()?)?.clone() + self.get_scene()?.clips.get(self.selection().track()?)?.clone() } /// Put a clip in a slot pub fn clip_put ( diff --git a/crates/device/src/arranger/arranger_select.rs b/crates/device/src/arranger/arranger_select.rs index 856e53eb..db9536ba 100644 --- a/crates/device/src/arranger/arranger_select.rs +++ b/crates/device/src/arranger/arranger_select.rs @@ -1,19 +1,16 @@ use crate::*; -impl>> HasSelection for T {} +impl> HasSelection for T {} -pub trait HasSelection: Has> { - fn selection (&self) -> Option<&Selection> { - self.get().as_ref() +pub trait HasSelection: Has { + fn selection (&self) -> &Selection { + self.get() } - fn selection_mut (&mut self) -> &mut Option { + fn selection_mut (&mut self) -> &mut Selection { self.get_mut() } } -impl Arrangement { -} - /// Represents the current user selection in the arranger #[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { diff --git a/crates/device/src/arranger/arranger_track.rs b/crates/device/src/arranger/arranger_track.rs index 26449380..20b1157d 100644 --- a/crates/device/src/arranger/arranger_track.rs +++ b/crates/device/src/arranger/arranger_track.rs @@ -149,13 +149,27 @@ impl Track { } } -impl> + Send + Sync> HasTrack for T {} +pub trait HasTrack { + fn track (&self) -> Option<&Track>; + fn track_mut (&mut self) -> Option<&mut Track>; +} -pub trait HasTrack: Has> + Send + Sync { +//impl>> HasTrack for T { + //fn track (&self) -> Option<&Track> { + //self.get().as_ref() + //} + //fn track_mut (&mut self) -> Option<&mut Track> { + //self.get_mut().as_mut() + //} +//} + +impl> + Has> HasTrack for T { fn track (&self) -> Option<&Track> { - Has::>::get(self).as_ref() + let index = Has::::get(self).track()?; + Has::>::get(self).get(index) } fn track_mut (&mut self) -> Option<&mut Track> { - Has::>::get_mut(self).as_mut() + let index = Has::::get(self).track()?; + Has::>::get_mut(self).get_mut(index) } } diff --git a/crates/device/src/arranger/arranger_tracks.rs b/crates/device/src/arranger/arranger_tracks.rs index 56fce892..09a3d273 100644 --- a/crates/device/src/arranger/arranger_tracks.rs +++ b/crates/device/src/arranger/arranger_tracks.rs @@ -47,11 +47,11 @@ pub trait HasTracks: Has> + Send + Sync { /// Iterate over tracks with their corresponding sizes. fn tracks_with_sizes ( &self, - selection: &Option, + selection: &Selection, editor_width: Option ) -> impl TracksSizes<'_> { let mut x = 0; - let active_track = if let Some(width) = editor_width && let Some(selection) = selection { + let active_track = if let Some(width) = editor_width { selection.track() } else { None @@ -74,7 +74,10 @@ impl<'a> ArrangerView<'a> { -> impl TracksSizes<'a> { self.arrangement - .tracks_with_sizes(&self.arrangement.selected, self.is_editing.then_some(20/*FIXME*/)) + .tracks_with_sizes( + &self.arrangement.selection(), + self.is_editing.then_some(20/*FIXME*/) + ) .map_while(move|(t, track, x1, x2)|{ (self.width_mid > x2 as u16).then_some((t, track, x1, x2)) }) diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs index b05905af..7e5edc22 100644 --- a/crates/device/src/arranger/arranger_view.rs +++ b/crates/device/src/arranger/arranger_view.rs @@ -35,7 +35,6 @@ impl<'a> ArrangerView<'a> { editor: Option<&'a MidiEditor> ) -> Self { let is_editing = editor.is_some(); - let selected = arrangement.selected; let h_tracks_area = arrangement.h_tracks_area(); let h_scenes_area = arrangement.h_scenes_area(); let h_scenes = arrangement.h_scenes(is_editing); @@ -54,7 +53,7 @@ impl<'a> ArrangerView<'a> { outputs_count: arrangement.midi_outs.len(), scenes_height: h_scenes_area, - scene_selected: selected.map(|s|s.scene()).flatten(), + scene_selected: arrangement.selection().scene(), scene_count: arrangement.scenes.len(), scene_last: arrangement.scenes.len().saturating_sub(1), scene_scroll: Fill::y(Fixed::x(1, ScrollbarV { @@ -65,7 +64,7 @@ impl<'a> ArrangerView<'a> { tracks_height: h_tracks_area, track_count: arrangement.tracks.len(), - track_selected: selected.map(|s|s.track()).flatten(), + track_selected: arrangement.selection().track(), track_scroll: Fill::x(Fixed::y(1, ScrollbarH { offset: arrangement.track_scroll, length: h_tracks_area as usize, @@ -160,9 +159,9 @@ impl<'a> ArrangerView<'a> { track_selected, is_editing, .. } = self; - let selection = Has::>::get(self.arrangement); - let selected_track = selection.map(|s|s.track()).flatten(); - let selected_scene = selection.map(|s|s.scene()).flatten(); + let selection = Has::::get(self.arrangement); + let selected_track = selection.track(); + let selected_scene = selection.scene(); Tryptich::center(*scenes_height) .left(*width_side, Map::new( diff --git a/crates/device/src/browser/browser_model.rs b/crates/device/src/browser/browser_model.rs index c06caff6..213d4276 100644 --- a/crates/device/src/browser/browser_model.rs +++ b/crates/device/src/browser/browser_model.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use std::ffi::OsString; /// Browses for phrase to import/export -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Browser { pub cwd: PathBuf, pub dirs: Vec<(OsString, String)>, From d7bbc2a412af827bc23f1c66f8b2eec720b4265c Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 14 May 2025 17:39:51 +0300 Subject: [PATCH 10/12] groovebox: reenable pool --- config/config_groovebox.edn | 2 +- crates/app/src/model.rs | 6 +- crates/app/src/view.rs | 4 +- crates/cli/tek.rs | 127 ++++++++----------- crates/device/src/arranger/arranger_model.rs | 4 - crates/device/src/pool/pool_view.rs | 24 ++-- 6 files changed, 73 insertions(+), 94 deletions(-) diff --git a/config/config_groovebox.edn b/config/config_groovebox.edn index fec1ca2d..d6d72f4d 100644 --- a/config/config_groovebox.edn +++ b/config/config_groovebox.edn @@ -10,7 +10,7 @@ (bsp/e :view-meters-input (bsp/n :view-sample-info (bsp/n (fixed/y 5 :view-sample-viewer) - (bsp/w (fixed/x :w-sidebar :view-pool) + (bsp/w :view-pool (bsp/e :view-samples-keys (fill/y :view-editor))))))))))) diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index c04f1fb3..8c39ba87 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -171,15 +171,15 @@ content!(TuiOut: |self: Message| match self { Self::FailedToAddDevice => "Failed #[tengri_proc::expose] impl App { - fn _todo_u16_stub (&self) -> u16 { - todo!() - } fn _todo_isize_stub (&self) -> isize { todo!() } fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + fn w_sidebar (&self) -> u16 { + self.project.w_sidebar(self.editor.is_some()) + } fn focus_editor (&self) -> bool { self.is_editing() } diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index 55980e5e..2d682638 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -33,9 +33,7 @@ impl App { ArrangerView::new(&self.project, self.editor.as_ref()) } pub fn view_pool (&self) -> impl Content + use<'_> { - let is_editing = self.is_editing(); - Fixed::x(self.project.w_sidebar(is_editing), - PoolView(is_editing, &self.project.pool)) + PoolView(&self.project.pool) } pub fn view_samples_keys (&self) -> impl Content + use<'_> { self.project.sampler().map(|s|s.view_list(false, self.editor().unwrap())) diff --git a/crates/cli/tek.rs b/crates/cli/tek.rs index c1954cb7..1d407373 100644 --- a/crates/cli/tek.rs +++ b/crates/cli/tek.rs @@ -72,43 +72,24 @@ pub enum LaunchMode { impl Cli { pub fn run (&self) -> Usually<()> { - let name = self.name.as_ref().map_or("tek", |x|x.as_str()); - let mode = &self.mode; - let empty = &[] as &[&str]; - let midi_froms = PortConnect::collect(&self.midi_from, empty, &self.midi_from_re); - let midi_tos = PortConnect::collect(&self.midi_to, empty, &self.midi_to_re); - let left_froms = PortConnect::collect(&self.left_from, empty, empty); - let left_tos = PortConnect::collect(&self.left_to, empty, empty); - let right_froms = PortConnect::collect(&self.right_from, empty, empty); - let right_tos = PortConnect::collect(&self.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()]; - let clip = match mode { - LaunchMode::Sequencer | LaunchMode::Groovebox => - Some(Arc::new(RwLock::new(MidiClip::new( - "Clip", true, 384usize, None, Some(ItemColor::random().into())), - ))), - _ => None, - }; - let scenes = vec![]; - let mut tracks = vec![]; - match mode { - LaunchMode::Sequencer => tracks.push(Track::new( - &name, None, jack, - Some(&clock), clip.as_ref(), - midi_froms.as_slice(), midi_tos.as_slice() - )?), - LaunchMode::Groovebox | LaunchMode::Sampler => tracks.push(Track::new_with_sampler( - &name, None, jack, - Some(&clock), clip.as_ref(), - midi_froms.as_slice(), midi_tos.as_slice(), - audio_froms, audio_tos, - )?), - _ => {} - } + let name = self.name.as_ref().map_or("tek", |x|x.as_str()); + let empty = &[] as &[&str]; + let mut midi_ins = vec![]; + let mut midi_outs = vec![]; + let mut tracks = vec![]; + let mut scenes = vec![]; + let midi_froms = PortConnect::collect(&self.midi_from, empty, &self.midi_from_re); + let midi_tos = PortConnect::collect(&self.midi_to, empty, &self.midi_to_re); + let left_froms = PortConnect::collect(&self.left_from, empty, empty); + let left_tos = PortConnect::collect(&self.left_to, empty, empty); + let right_froms = PortConnect::collect(&self.right_from, empty, empty); + let right_tos = PortConnect::collect(&self.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()]; + let clip = Arc::new(RwLock::new(MidiClip::new( + "Clip", true, 384usize, None, Some(ItemColor::random().into())), + )); Tui::new()?.run(&Jack::new(name)?.run(|jack|{ - let mut midi_ins = vec![]; - let mut midi_outs = vec![]; for (index, connect) in midi_froms.iter().enumerate() { let port = JackMidiIn::new(jack, &format!("M/{index}"), &[connect.clone()])?; midi_ins.push(port); @@ -117,53 +98,57 @@ impl Cli { let port = JackMidiOut::new(jack, &format!("{index}/M"), &[connect.clone()])?; midi_outs.push(port); }; - let config_path = match mode { + let config = Configuration::new(&match self.mode { LaunchMode::Clock => "config/config_transport.edn", LaunchMode::Sequencer => "config/config_sequencer.edn", LaunchMode::Groovebox => "config/config_groovebox.edn", LaunchMode::Arranger { .. } => "config/config_arranger.edn", LaunchMode::Sampler => "config/config_sampler.edn", - _ => todo!("{mode:?}"), - }; - let config = Configuration::new(&config_path, false)?; - let clock = Clock::new(jack, self.bpm)?; + _ => todo!("{:?}", self.mode), + }, false)?; + let clock = Clock::new(jack, self.bpm)?; + match self.mode { + LaunchMode::Sequencer => tracks.push(Track::new( + &name, None, jack, Some(&clock), Some(&clip), + midi_froms.as_slice(), midi_tos.as_slice() + )?), + LaunchMode::Groovebox | LaunchMode::Sampler => tracks.push(Track::new_with_sampler( + &name, None, jack, Some(&clock), Some(&clip), + midi_froms.as_slice(), midi_tos.as_slice(), audio_froms, audio_tos, + )?), + _ => {} + } let mut app = App { - jack: jack.clone(), + jack: jack.clone(), + config, color: ItemTheme::random(), - pool: match mode { - LaunchMode::Sequencer | LaunchMode::Groovebox => clip.as_ref().map(Into::into), - LaunchMode::Arranger { .. } => Some(Default::default()), - _ => None, - }, - editor: match mode { - LaunchMode::Sequencer | LaunchMode::Groovebox => clip.as_ref().map(Into::into), - LaunchMode::Arranger { .. } => Some(Default::default()), + editor: match self.mode { + LaunchMode::Sequencer | LaunchMode::Groovebox => Some((&clip).into()), _ => None }, - midi_ins, - midi_outs, - midi_buf: match mode { - LaunchMode::Clock | - LaunchMode::Sampler => - vec![], - LaunchMode::Sequencer | - LaunchMode::Groovebox | - LaunchMode::Arranger {..} => - vec![vec![];65536], - _ => todo!("{mode:?}"), + project: Arrangement { + name: Default::default(), + color: ItemTheme::random(), + jack: jack.clone(), + clock, + tracks, + scenes, + selection: Selection::TrackClip { track: 0, scene: 0 }, + midi_ins, + midi_outs, + pool: match self.mode { + LaunchMode::Sequencer | LaunchMode::Groovebox => (&clip).into(), + _ => Default::default() + }, + ..Default::default() }, - tracks, - scenes, - selected: Selection::TrackClip { track: 0, scene: 0 }, - config, - clock, ..Default::default() }; - if let &LaunchMode::Arranger { scenes, tracks, track_width, .. } = mode { - app.arranger = Default::default(); - app.selected = Selection::TrackClip { track: 1, scene: 1 }; - app.scenes_add(scenes)?; - app.tracks_add(tracks, Some(track_width), &[], &[])?; + if let LaunchMode::Arranger { scenes, tracks, track_width, .. } = self.mode { + app.project.arranger = Default::default(); + app.project.selection = Selection::TrackClip { track: 1, scene: 1 }; + app.project.scenes_add(scenes)?; + app.project.tracks_add(tracks, Some(track_width), &[], &[])?; } jack.sync_lead(self.sync_lead, |mut state|{ let clock = app.clock(); diff --git a/crates/device/src/arranger/arranger_model.rs b/crates/device/src/arranger/arranger_model.rs index a1d680fc..4e3113c4 100644 --- a/crates/device/src/arranger/arranger_model.rs +++ b/crates/device/src/arranger/arranger_model.rs @@ -18,10 +18,6 @@ pub struct Arrangement { pub audio_ins: Vec, /// List of global audio outputs pub audio_outs: Vec, - /// Buffer for writing a midi event - pub note_buf: Vec, - /// Buffer for writing a chunk of midi events - pub midi_buf: Vec>>, /// Last track number (to avoid duplicate port names) pub track_last: usize, /// List of tracks diff --git a/crates/device/src/pool/pool_view.rs b/crates/device/src/pool/pool_view.rs index 82e74ae0..29e4aa01 100644 --- a/crates/device/src/pool/pool_view.rs +++ b/crates/device/src/pool/pool_view.rs @@ -1,31 +1,31 @@ use crate::*; -pub struct PoolView<'a>(pub bool, pub &'a Pool); +pub struct PoolView<'a>(pub &'a Pool); content!(TuiOut: |self: PoolView<'a>| { - let Self(compact, model) = self; - let Pool { clips, .. } = self.1; + let Self(pool) = self; //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); - let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); - let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); - let iter = | |model.clips().clone().into_iter(); - let height = clips.read().unwrap().len() as u16; - Tui::bg(Reset, Fixed::y(height, on_bg(border(Map::new(iter, move|clip: Arc>, i|{ + //let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); + //let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); + let height = pool.clips.read().unwrap().len() as u16; + Fixed::x(20, Fill::y(Align::c(Map::new( + ||pool.clips().clone().into_iter(), + move|clip: Arc>, i: usize|{ let item_height = 1; let item_offset = i as u16 * item_height; - let selected = i == model.clip_index(); + let selected = i == pool.clip_index(); let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); let bg = if selected { color.light.rgb } else { color.base.rgb }; let fg = color.lightest.rgb; - let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; - let length = if *compact { String::default() } else { format!("{length} ") }; + let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; + let length = if false { String::default() } else { format!("{length} ") }; Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), Fill::x(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), )))) - }))))) + })))) }); content!(TuiOut: |self: ClipLength| { From e3a3962130138f601473b1c8fed82243db1e02c5 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 14 May 2025 17:59:06 +0300 Subject: [PATCH 11/12] simplify --- config/config_groovebox.edn | 20 +++++----- config/keys_global.edn | 12 +++--- crates/app/src/api.rs | 4 +- crates/app/src/lib.rs | 5 +-- crates/app/src/model.rs | 41 +++++++++++--------- crates/app/src/view.rs | 11 +++--- crates/device/src/arranger/arranger_model.rs | 1 + crates/device/src/arranger/arranger_port.rs | 9 ++++- crates/device/src/arranger/arranger_view.rs | 16 ++------ crates/device/src/editor/editor_model.rs | 2 + crates/device/src/editor/editor_view.rs | 2 - crates/device/src/editor/editor_view_h.rs | 4 +- crates/device/src/lib.rs | 3 +- crates/engine/src/lib.rs | 23 +++++------ deps/tengri | 2 +- 15 files changed, 74 insertions(+), 81 deletions(-) diff --git a/config/config_groovebox.edn b/config/config_groovebox.edn index d6d72f4d..7afce303 100644 --- a/config/config_groovebox.edn +++ b/config/config_groovebox.edn @@ -3,16 +3,16 @@ (info "A sequencer with built-in sampler.") (view - (bsp/a :view-dialog - (bsp/s (fixed/y 1 :view-transport) - (bsp/n (fixed/y 1 :view-status) - (bsp/w :view-meters-output - (bsp/e :view-meters-input - (bsp/n :view-sample-info - (bsp/n (fixed/y 5 :view-sample-viewer) - (bsp/w :view-pool - (bsp/e :view-samples-keys - (fill/y :view-editor))))))))))) + (bsp/a :view-dialog + (bsp/s (fixed/y 1 :view-transport) + (bsp/n (fixed/y 1 :view-status) + (bsp/w :view-meters-output + (bsp/e :view-meters-input + (bsp/n :view-sample-info + (bsp/n (fixed/y 5 :view-sample-viewer) + (bsp/w :view-pool + (bsp/e :view-samples-keys + (fill/y :view-editor))))))))))) (keys (layer-if :focus-browser "./keys_browser.edn") diff --git a/config/keys_global.edn b/config/keys_global.edn index af89f616..8e9f4233 100644 --- a/config/keys_global.edn +++ b/config/keys_global.edn @@ -1,9 +1,9 @@ -(@esc cancel-dialog) -(@f1 toggle-dialog :dialog-help) -(@f6 toggle-dialog :dialog-save) -(@f8 toggle-dialog :dialog-options) -(@f9 toggle-dialog :dialog-load) -(@f10 toggle-dialog :dialog-quit) +(@esc dialog :dialog-none) +(@f1 dialog :dialog-help) +(@f6 dialog :dialog-save) +(@f8 dialog :dialog-options) +(@f9 dialog :dialog-load) +(@f10 dialog :dialog-quit) (@u undo 1) (@r redo 1) diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index 90b086a0..6b04fe75 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -18,8 +18,8 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm })); #[tengri_proc::command(App)] impl AppCommand { - fn toggle_dialog (app: &mut App, dialog: Dialog) -> Perhaps { - app.toggle_dialog(Some(dialog)); + fn dialog (app: &mut App, dialog: Option) -> Perhaps { + app.toggle_dialog(dialog); Ok(None) } fn cancel_dialog (app: &mut App) -> Perhaps { diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index f1d59d7f..602ce67a 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -16,12 +16,9 @@ #![feature(type_changing_struct_update)] #![feature(let_chains)] #![feature(closure_lifetime_binder)] -/// Standard result type. -pub type Usually = std::result::Result>; -/// Standard optional result type. -pub type Perhaps = std::result::Result, Box>; pub use ::tek_engine:: *; pub use ::tek_device::{self, *}; +pub use ::tengri::{Usually, Perhaps, Has}; pub use ::tengri::dsl::*; pub use ::tengri::input::*; pub use ::tengri::output::*; diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index 8c39ba87..5a459295 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -35,8 +35,8 @@ has!(Vec: |self: App|self.project.midi_ins); has!(Vec: |self: App|self.project.midi_outs); has!(Vec: |self: App|self.project.scenes); has!(Vec: |self: App|self.project.tracks); +has!(Measure: |self: App|self.size); -has_size!(|self: App|&self.size); has_clips!(|self: App|self.project.pool.clips); has_editor!(|self: App|{ editor = self.editor; @@ -152,8 +152,8 @@ impl App { /// Various possible dialog overlays #[derive(Clone, Debug)] pub enum Dialog { - Help, - Menu, + Help(usize), + Menu(usize), Device(usize), Message(Message), Save(Browser), @@ -216,29 +216,32 @@ impl App { fn focus_pool_length (&self) -> bool { matches!(self.project.pool.mode, Some(PoolMode::Length(..))) } - fn dialog_device (&self) -> Dialog { - Dialog::Device(0) // TODO + fn dialog_none (&self) -> Option { + None } - fn dialog_device_prev (&self) -> Dialog { - Dialog::Device(0) // TODO + fn dialog_device (&self) -> Option { + Some(Dialog::Device(0)) // TODO } - fn dialog_device_next (&self) -> Dialog { - Dialog::Device(0) // TODO + fn dialog_device_prev (&self) -> Option { + Some(Dialog::Device(0)) // TODO } - fn dialog_help (&self) -> Dialog { - Dialog::Help + fn dialog_device_next (&self) -> Option { + Some(Dialog::Device(0)) // TODO } - fn dialog_menu (&self) -> Dialog { - Dialog::Menu + fn dialog_help (&self) -> Option { + Some(Dialog::Help(0)) } - fn dialog_save (&self) -> Dialog { - Dialog::Save(Default::default()) + fn dialog_menu (&self) -> Option { + Some(Dialog::Menu(0)) } - fn dialog_load (&self) -> Dialog { - Dialog::Load(Default::default()) + fn dialog_save (&self) -> Option { + Some(Dialog::Save(Default::default())) } - fn dialog_options (&self) -> Dialog { - Dialog::Options + fn dialog_load (&self) -> Option { + Some(Dialog::Load(Default::default())) + } + fn dialog_options (&self) -> Option { + Some(Dialog::Options) } fn editor_pitch (&self) -> Option { Some((self.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index 2d682638..3ffbd439 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -58,10 +58,10 @@ impl App { Fixed::xy(70, 23, Tui::fg_bg(Rgb(255,255,255), Rgb(16,16,16), Bsp::b( Repeat(" "), Outer(true, Style::default().fg(Tui::g(96))) .enclose(self.dialog.as_ref().map(|dialog|match dialog { - Dialog::Menu => + Dialog::Menu(_) => self.view_dialog_menu().boxed(), - Dialog::Help => - self.view_dialog_help().boxed(), + Dialog::Help(offset) => + self.view_dialog_help(*offset).boxed(), Dialog::Save(browser) => self.view_dialog_save().boxed(), Dialog::Load(browser) => @@ -84,12 +84,13 @@ impl App { let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) } - pub fn view_dialog_help <'a> (&'a self) -> impl Content + use<'a> { + pub fn view_dialog_help <'a> (&'a self, offset: usize) -> impl Content + use<'a> { Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1, - ||self.config.keys.layers.iter() + move||self.config.keys.layers.iter() .filter_map(|a|(a.0)(self).then_some(a.1)) .flat_map(|a|a) .filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None }) + .skip(offset) .take(20), |mut b,i|Fixed::x(60, Align::w(Bsp::e("(", Bsp::e( b.next().map(|t|Fixed::x(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), diff --git a/crates/device/src/arranger/arranger_model.rs b/crates/device/src/arranger/arranger_model.rs index 4e3113c4..46687ab1 100644 --- a/crates/device/src/arranger/arranger_model.rs +++ b/crates/device/src/arranger/arranger_model.rs @@ -46,6 +46,7 @@ has!(Vec: |self: Arrangement|self.midi_ins); has!(Vec: |self: Arrangement|self.midi_outs); has!(Vec: |self: Arrangement|self.scenes); has!(Vec: |self: Arrangement|self.tracks); +has!(Measure: |self: Arrangement|self.size); impl Arrangement { /// Width of display diff --git a/crates/device/src/arranger/arranger_port.rs b/crates/device/src/arranger/arranger_port.rs index c98b0807..152edc73 100644 --- a/crates/device/src/arranger/arranger_port.rs +++ b/crates/device/src/arranger/arranger_port.rs @@ -18,7 +18,7 @@ impl<'a> ArrangerView<'a> { pub(crate) fn input_ports (&'a self) -> impl Content + 'a { Tryptich::top(1) .left(self.width_side, - button_3("i", "midi ins", format!("{}", self.inputs_count), self.is_editing)) + button_3("i", "midi ins", format!("{}", self.arrangement.midi_ins().len()), self.is_editing)) .right(self.width_side, button_2("I", "add midi in", self.is_editing)) .middle(self.width_mid, @@ -96,7 +96,12 @@ impl<'a> ArrangerView<'a> { } pub(crate) fn output_count (&'a self) -> impl Content + 'a { - button_3("o", "midi outs", format!("{}", self.outputs_count), self.is_editing) + button_3( + "o", + "midi outs", + format!("{}", self.arrangement.midi_outs().len()), + self.is_editing + ) } pub(crate) fn output_add (&'a self) -> impl Content + 'a { diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs index 7e5edc22..a903d26f 100644 --- a/crates/device/src/arranger/arranger_view.rs +++ b/crates/device/src/arranger/arranger_view.rs @@ -9,20 +9,15 @@ pub struct ArrangerView<'a> { pub width_mid: u16, pub width_side: u16, - pub inputs_count: usize, pub inputs_height: u16, - - pub outputs_count: usize, pub outputs_height: u16, pub scene_last: usize, - pub scene_count: usize, pub scene_scroll: Fill>, pub scene_selected: Option, pub scenes_height: u16, pub track_scroll: Fill>, - pub track_count: usize, pub track_selected: Option, pub tracks_height: u16, @@ -47,14 +42,10 @@ impl<'a> ArrangerView<'a> { width_side: arrangement.w_sidebar(is_editing), inputs_height: arrangement.h_inputs(), - inputs_count: arrangement.midi_ins.len(), - outputs_height: arrangement.h_outputs(), - outputs_count: arrangement.midi_outs.len(), scenes_height: h_scenes_area, scene_selected: arrangement.selection().scene(), - scene_count: arrangement.scenes.len(), scene_last: arrangement.scenes.len().saturating_sub(1), scene_scroll: Fill::y(Fixed::x(1, ScrollbarV { offset: arrangement.scene_scroll, @@ -63,7 +54,6 @@ impl<'a> ArrangerView<'a> { })), tracks_height: h_tracks_area, - track_count: arrangement.tracks.len(), track_selected: arrangement.selection().track(), track_scroll: Fill::x(Fixed::y(1, ScrollbarH { offset: arrangement.track_scroll, @@ -108,9 +98,9 @@ impl<'a> ArrangerView<'a> { /// Render track headers pub(crate) fn tracks (&'a self) -> impl Content + 'a { - let Self { width_side, width_mid, track_count, track_selected, is_editing, .. } = self; + let Self { width_side, width_mid, track_selected, is_editing, .. } = self; Tryptich::center(3) - .left(*width_side, button_3("t", "track", format!("{}", *track_count), *is_editing)) + .left(*width_side, button_3("t", "track", format!("{}", self.arrangement.tracks.len()), *is_editing)) .right(*width_side, button_2("T", "add track", *is_editing)) .middle(*width_mid, per_track(*width_mid, ||self.tracks_with_sizes_scrolled(), |index, track|wrap( @@ -126,7 +116,7 @@ impl<'a> ArrangerView<'a> { /// Render device switches. pub(crate) fn devices (&'a self) -> impl Content + 'a { - let Self { width_side, width_mid, track_count, track_selected, is_editing, .. } = self; + let Self { width_side, width_mid, track_selected, is_editing, .. } = self; Tryptich::top(1) .left(*width_side, button_3("d", "devices", format!("{}", 0), *is_editing)) .right(*width_side, button_2("D", "add device", *is_editing)) diff --git a/crates/device/src/editor/editor_model.rs b/crates/device/src/editor/editor_model.rs index 9d8d31f6..c0444a9b 100644 --- a/crates/device/src/editor/editor_model.rs +++ b/crates/device/src/editor/editor_model.rs @@ -8,6 +8,8 @@ pub struct MidiEditor { pub mode: PianoHorizontal, } +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") diff --git a/crates/device/src/editor/editor_view.rs b/crates/device/src/editor/editor_view.rs index 7828d416..c91de260 100644 --- a/crates/device/src/editor/editor_view.rs +++ b/crates/device/src/editor/editor_view.rs @@ -1,7 +1,5 @@ use crate::*; -has_size!(|self: MidiEditor|&self.size); - content!(TuiOut: |self: MidiEditor| { self.autoscroll(); //self.autozoom(); diff --git a/crates/device/src/editor/editor_view_h.rs b/crates/device/src/editor/editor_view_h.rs index b9bf650b..35f430bd 100644 --- a/crates/device/src/editor/editor_view_h.rs +++ b/crates/device/src/editor/editor_view_h.rs @@ -18,6 +18,8 @@ pub struct PianoHorizontal { pub keys_width: u16, } +has!(Measure:|self:PianoHorizontal|self.size); + impl PianoHorizontal { pub fn new (clip: Option<&Arc>>) -> Self { let size = Measure::new(); @@ -227,8 +229,6 @@ impl PianoHorizontal { } } -has_size!(|self:PianoHorizontal|&self.size); - impl TimeRange for PianoHorizontal { fn time_len (&self) -> &AtomicUsize { self.range.time_len() } fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 7c2a0c01..5079ccd0 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -12,7 +12,8 @@ pub(crate) use std::path::PathBuf; pub(crate) use std::error::Error; pub(crate) use std::ffi::OsString; -pub(crate) use ::tengri::{from, Usually, Perhaps, dsl::*, input::*, output::*, tui::{*, ratatui::prelude::*}}; +pub(crate) use ::tengri::{from, Usually, Perhaps, Has}; +pub(crate) use ::tengri::{dsl::*, input::*, output::*, tui::{*, ratatui::prelude::*}}; pub(crate) use ::tek_engine::*; pub(crate) use ::tek_engine::midi::{u7, LiveEvent, MidiMessage}; pub(crate) use ::tek_engine::jack::{Control, ProcessScope, MidiWriter, RawMidi}; diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 8f891e4c..9acdc1cb 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -9,24 +9,19 @@ pub(crate) use std::sync::{Arc, atomic::{AtomicUsize, AtomicBool, Ordering::Rela pub(crate) use std::fmt::Debug; pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem}; -pub(crate) use ::tengri::{from, Usually, Perhaps, tui::*}; +pub(crate) use ::tengri::{from, Usually, Perhaps, Has, tui::*}; pub use ::atomic_float; pub(crate) use atomic_float::*; -pub trait Has: Send + Sync { - fn get (&self) -> &T; - fn get_mut (&mut self) -> &mut T; -} +//pub trait MaybeHas: Send + Sync { + //fn get (&self) -> Option<&T>; +//} -pub trait MaybeHas: Send + Sync { - fn get (&self) -> Option<&T>; -} - -impl>> MaybeHas for U { - fn get (&self) -> Option<&T> { - Has::>::get(self).as_ref() - } -} +//impl>> MaybeHas for U { + //fn get (&self) -> Option<&T> { + //Has::>::get(self).as_ref() + //} +//} #[macro_export] macro_rules! has { ($T:ty: |$self:ident : $S:ty| $x:expr) => { diff --git a/deps/tengri b/deps/tengri index a9619ab9..8bfd1a23 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit a9619ab9cea40aed3ec30483f4a7db03ca7fec9d +Subproject commit 8bfd1a23a1f880a1d2fb104a158fc51f244acd6e From 0f16c89248bc2289760bd10b6e3b82528cbbdf3d Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 14 May 2025 18:00:04 +0300 Subject: [PATCH 12/12] move global keys to end --- config/config_arranger.edn | 4 ++-- config/config_groovebox.edn | 4 ++-- config/config_sampler.edn | 4 ++-- config/config_sequencer.edn | 3 +-- config/config_transport.edn | 4 ++-- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/config/config_arranger.edn b/config/config_arranger.edn index cffba38f..51932ec3 100644 --- a/config/config_arranger.edn +++ b/config/config_arranger.edn @@ -16,11 +16,11 @@ (layer-if :focus-browser "./keys_browser.edn") (layer-if :focus-pool-rename "./keys_rename.edn") (layer-if :focus-pool-length "./keys_length.edn") - (layer "./keys_global.edn") (layer-if :focus-editor "./keys_editor.edn") (layer-if :focus-clip "./keys_clip.edn") (layer-if :focus-track "./keys_track.edn") (layer-if :focus-scene "./keys_scene.edn") (layer-if :focus-mix "./keys_mix.edn") (layer "./keys_clock.edn") - (layer "./keys_arranger.edn")) + (layer "./keys_arranger.edn") + (layer "./keys_global.edn")) diff --git a/config/config_groovebox.edn b/config/config_groovebox.edn index 7afce303..c313319f 100644 --- a/config/config_groovebox.edn +++ b/config/config_groovebox.edn @@ -18,7 +18,7 @@ (layer-if :focus-browser "./keys_browser.edn") (layer-if :focus-pool-rename "./keys_rename.edn") (layer-if :focus-pool-length "./keys_length.edn") - (layer "./keys_global.edn") (layer "./keys_clock.edn") (layer "./keys_editor.edn") - (layer "./keys_sampler.edn")) + (layer "./keys_sampler.edn") + (layer "./keys_global.edn")) diff --git a/config/config_sampler.edn b/config/config_sampler.edn index 8f6c209a..c2486385 100644 --- a/config/config_sampler.edn +++ b/config/config_sampler.edn @@ -9,5 +9,5 @@ (fill/xy :view-samples-grid))))) (keys - (layer "./keys_global.edn") - (layer "./keys_sampler.edn")) + (layer "./keys_sampler.edn") + (layer "./keys_global.edn")) diff --git a/config/config_sequencer.edn b/config/config_sequencer.edn index f20e2fcd..a267cebd 100644 --- a/config/config_sequencer.edn +++ b/config/config_sequencer.edn @@ -14,7 +14,6 @@ (layer-if :focus-browser "./keys_browser.edn") (layer-if :mode-pool-rename "./keys_rename.edn") (layer-if :mode-pool-length "./keys_length.edn") - (layer "./keys_global.edn") (layer "./keys_editor.edn") (layer "./keys_clock.edn") - (layer "./keys_arranger.edn")) + (layer "./keys_global.edn")) diff --git a/config/config_transport.edn b/config/config_transport.edn index b84f0279..47324e7b 100644 --- a/config/config_transport.edn +++ b/config/config_transport.edn @@ -5,5 +5,5 @@ (view :view-transport) (keys - (layer "./keys_global.edn") - (layer "./keys_clock.edn")) + (layer "./keys_clock.edn") + (layer "./keys_global.edn"))