diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index 90b086a0..17ef811a 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,47 +42,49 @@ 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.launch(); + Ok(None) + } fn select (app: &mut App, selection: Selection) -> Perhaps { - *app.project.selection_mut() = selection; - if let Some(ref mut editor) = app.editor { - 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)) - => - Some(clip), - _ => - None - }); - } + app.select(selection); 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.tracks_stop_all(); + app.stop_all(); Ok(None) } fn sampler (app: &mut App, command: SamplerCommand) -> Perhaps { - Ok(app.project.sampler_mut() + Ok(app.sampler_mut() .map(|s|command.delegate(s, |command|Self::Sampler{command})) .transpose()? .flatten()) } - fn project (app: &mut App, command: ArrangementCommand) -> Perhaps { - Ok(command.delegate(&mut app.project, |command|Self::Project{command})?) + 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 clock (app: &mut App, command: ClockCommand) -> Perhaps { - Ok(command.execute(app.clock_mut())?.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})?) } fn message (app: &mut App, command: MessageCommand) -> Perhaps { Ok(command.delegate(app, |command|Self::Message{command})?) @@ -91,7 +93,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.project.sampler_mut().map(|sampler|match command { + app.sampler_mut().map(|sampler|match command { // autoselect: automatically select sample in sampler MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, _ => {} @@ -102,26 +104,27 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm }) } fn pool (app: &mut App, command: PoolCommand) -> Perhaps { - 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) + Ok(if let Some(pool) = app.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 + }) } } 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) } } @@ -133,25 +136,19 @@ impl<'state> Context<'state, MidiEditCommand> for App { impl<'state> Context<'state, PoolCommand> for App { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { - Context::get(&self.project.pool, iter) + self.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.project.sampler().map(|p|Context::get(p, iter)).flatten() - } -} - -impl<'state> Context<'state, ArrangementCommand> for App { - fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { - Context::get(&self.project, iter) + self.sampler().map(|p|Context::get(p, iter)).flatten() } } #[tengri_proc::command(App)] impl MessageCommand { fn dismiss (app: &mut App) -> Perhaps { - app.dialog = None; + app.message_dismiss(); Ok(None) } } diff --git a/crates/app/src/audio.rs b/crates/app/src/audio.rs index dd65d0af..15d15232 100644 --- a/crates/app/src/audio.rs +++ b/crates/app/src/audio.rs @@ -1,10 +1,16 @@ 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.project.midi_input_collect(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() { @@ -20,16 +26,14 @@ audio!( editor.set_note_pos(pitch.as_int() as usize); } } - let result = self.project.process_tracks(client, scope); + let result = self.arranger.tracks_render(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 c04f1fb3..18c6d61f 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -6,21 +6,27 @@ 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, - /// Contains the currently edited musical arrangement - pub project: Arrangement, + pub config: Configuration, /// Undo history - pub history: Vec, + pub history: Vec, // Dialog overlay - pub dialog: Option, + 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 editor: Option, + /// Contains the currently edited musical arrangement + pub arranger: Arrangement, // Cache of formatted strings pub view_cache: Arc>, @@ -28,48 +34,27 @@ pub struct App { pub color: ItemTheme, } -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|{ - 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.editor.is_some(); -}); +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, mut dialog: Option) -> Option { - std::mem::swap(&mut self.dialog, &mut dialog); - dialog - } - pub fn toggle_editor (&mut self, value: Option) { - //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(); + pub fn toggle_dialog (&mut self, dialog: Option) { + self.dialog = if self.dialog == dialog { + None } else { - self.clip_auto_remove(); + dialog } } - 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 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(); + }); } pub(crate) fn device_pick (&mut self, index: usize) { self.dialog = Some(Dialog::Device(index)); @@ -82,82 +67,22 @@ impl App { } 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_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.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( - &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] - ) { - self.dialog = None; - Device::Sampler(sampler) - } else { - self.dialog = Some(Dialog::Message(Message::FailedToAddDevice)); - return Err("failed to add device".into()) - }; - 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 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) = self.project.tracks.get_mut(track) - { - 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) = self.editor { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip.clone()); - Some(clip) - } else { - None - } - } - // Remove clip from arrangement when exiting empty clip editor - fn clip_auto_remove (&mut self) { - 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 { - self.project.pool.delete_clip(&clip.read().unwrap()); - } - } - } } /// Various possible dialog overlays -#[derive(Clone, Debug)] +#[derive(PartialEq, Clone, Copy, Debug)] pub enum Dialog { Help, Menu, Device(usize), Message(Message), - Save(Browser), - Load(Browser), + Save, + Load, Options, } @@ -168,12 +93,24 @@ 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 { - fn _todo_u16_stub (&self) -> u16 { - todo!() - } fn _todo_isize_stub (&self) -> isize { todo!() } @@ -190,31 +127,31 @@ impl App { matches!(self.dialog, Some(Dialog::Device(..))) } fn focus_browser (&self) -> bool { - self.browser().is_some() + self.browser.is_visible } fn focus_clip (&self) -> bool { - !self.is_editing() && self.selection().is_clip() + !self.is_editing() && self.selected.is_clip() } fn focus_track (&self) -> bool { - !self.is_editing() && self.selection().is_track() + !self.is_editing() && self.selected.is_track() } fn focus_scene (&self) -> bool { - !self.is_editing() && self.selection().is_scene() + !self.is_editing() && self.selected.is_scene() } fn focus_mix (&self) -> bool { - !self.is_editing() && self.selection().is_mix() + !self.is_editing() && self.selected.is_mix() } fn focus_pool_import (&self) -> bool { - matches!(self.project.pool.mode, Some(PoolMode::Import(..))) + matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Import(..))) } fn focus_pool_export (&self) -> bool { - matches!(self.project.pool.mode, Some(PoolMode::Export(..))) + matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Export(..))) } fn focus_pool_rename (&self) -> bool { - matches!(self.project.pool.mode, Some(PoolMode::Rename(..))) + matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Rename(..))) } fn focus_pool_length (&self) -> bool { - matches!(self.project.pool.mode, Some(PoolMode::Length(..))) + matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Length(..))) } fn dialog_device (&self) -> Dialog { Dialog::Device(0) // TODO @@ -232,47 +169,47 @@ impl App { Dialog::Menu } fn dialog_save (&self) -> Dialog { - Dialog::Save(Default::default()) + Dialog::Save } fn dialog_load (&self) -> Dialog { - Dialog::Load(Default::default()) + Dialog::Load } fn dialog_options (&self) -> Dialog { Dialog::Options } fn editor_pitch (&self) -> Option { - Some((self.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) + Some((self.editor().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 scene_selected (&self) -> Option { + self.selected.scene() } fn track_count (&self) -> usize { - self.tracks().len() + self.tracks.len() } - fn track_selection (&self) -> Option { - self.selection().track() + fn track_selected (&self) -> Option { + self.selected.track() } fn select_scene_next (&self) -> Selection { - self.selection().scene_next(self.scenes().len()) + self.selected.scene_next(self.scenes.len()) } fn select_scene_prev (&self) -> Selection { - self.selection().scene_prev() + self.selected.scene_prev() } fn select_track_header (&self) -> Selection { - self.selection().track_header(self.tracks().len()) + self.selected.track_header(self.tracks.len()) } fn select_track_next (&self) -> Selection { - self.selection().track_next(self.tracks().len()) + self.selected.track_next(self.tracks.len()) } fn select_track_prev (&self) -> Selection { - self.selection().track_prev() + self.selected.track_prev() } - fn clip_selection (&self) -> Option>> { - match self.selection() { - Selection::TrackClip { track, scene } => self.scenes()[*scene].clips[*track].clone(), + fn clip_selected (&self) -> Option>> { + match self.selected { + Selection::TrackClip { track, scene } => self.scenes[scene].clips[track].clone(), _ => None } } @@ -297,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/app/src/view.rs b/crates/app/src/view.rs index 55980e5e..ff8a7c07 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -17,70 +17,65 @@ impl App { pub fn view_status (&self) -> impl Content + use<'_> { self.update_clock(); let cache = self.view_cache.read().unwrap(); - view_status(Some(self.project.selection.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<'_> { self.update_clock(); let cache = self.view_cache.read().unwrap(); - view_transport(self.project.clock.is_rolling(), + view_transport(self.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.project.sampler().map(|s|s.view_list(false, self.editor().unwrap())) + self.sampler().map(|s|s.view_list(false, self.editor().unwrap())) } pub fn view_samples_grid (&self) -> impl Content + use<'_> { - self.project.sampler().map(|s|s.view_grid()) + self.sampler().map(|s|s.view_grid()) } pub fn view_sample_viewer (&self) -> impl Content + use<'_> { - self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) + self.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) } pub fn view_sample_info (&self) -> impl Content + use<'_> { - self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) + self.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) } pub fn view_meters_input (&self) -> impl Content + use<'_> { - self.project.sampler().map(|s|s.view_meters_input()) + self.sampler().map(|s|s.view_meters_input()) } pub fn view_meters_output (&self) -> impl Content + use<'_> { - self.project.sampler().map(|s|s.view_meters_output()) + self.sampler().map(|s|s.view_meters_output()) } pub fn view_dialog (&self) -> impl Content + use<'_> { - When(self.dialog.is_some(), Bsp::b( "", + 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(self.dialog.as_ref().map(|dialog|match dialog { + .enclose(app.dialog.as_ref().map(|dialog|match dialog { Dialog::Menu => - self.view_dialog_menu().boxed(), + app.view_dialog_menu().boxed(), Dialog::Help => - self.view_dialog_help().boxed(), - Dialog::Save(browser) => - self.view_dialog_save().boxed(), - Dialog::Load(browser) => - self.view_dialog_load().boxed(), + app.view_dialog_help().boxed(), + Dialog::Save => + app.view_dialog_save().boxed(), + Dialog::Load => + app.view_dialog_load().boxed(), Dialog::Options => - self.view_dialog_options().boxed(), + app.view_dialog_options().boxed(), Dialog::Device(index) => - self.view_dialog_device(*index).boxed(), + app.view_dialog_device(*index).boxed(), Dialog::Message(message) => - self.view_dialog_message(message).boxed(), + 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)); diff --git a/crates/device/src/arranger/arranger_api.rs b/crates/device/src/arranger/arranger_api.rs index daa31c67..bd6d4721 100644 --- a/crates/device/src/arranger/arranger_api.rs +++ b/crates/device/src/arranger/arranger_api.rs @@ -6,28 +6,36 @@ 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 select_nothing (&self) -> Selection { - Selection::Nothing - } } #[tengri_proc::command(Arrangement)] impl ArrangementCommand { /// Set the selection - fn select (arranger: &mut Arrangement, s: Selection) -> Perhaps { - *arranger.selection_mut() = s; + 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 { - match *arranger.selection() { - Selection::Track(t) => { + use Selection::*; + match arranger.selected { + Some(Track(t)) => { arranger.tracks[t].sequencer.enqueue_next(None) }, - Selection::TrackClip { track, scene } => { + Some(TrackClip { track, scene }) => { arranger.tracks[track].sequencer.enqueue_next(arranger.scenes[scene].clips[track].as_ref()) }, - Selection::Scene(s) => { + Some(Scene(s)) => { for t in 0..arranger.tracks.len() { arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref()) } @@ -37,45 +45,50 @@ impl ArrangementCommand { Ok(None) } /// Set the color of the selected entity - fn set_color (arranger: &mut Arrangement, palette: Option) -> Perhaps { - 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 + 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 }, - Selection::Scene(s) => { - std::mem::swap(&mut palette, &mut arranger.scenes[s].color); - palette + Some(Scene(s)) => { + let old = arranger.scenes[s].color; + arranger.scenes[s].color = palette; + old } - Selection::Track(t) => { - std::mem::swap(&mut palette, &mut arranger.tracks[t].color); - palette + Some(Track(t)) => { + let old = arranger.tracks[t].color; + arranger.tracks[t].color = palette; + old } - Selection::TrackClip { track, scene } => { + Some(TrackClip { track, scene }) => { if let Some(ref clip) = arranger.scenes[scene].clips[track] { let mut clip = clip.write().unwrap(); - std::mem::swap(&mut palette, &mut clip.color); - palette + let old = clip.color; + clip.color = palette; + old } else { - return Ok(None) + return 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.selection_mut() = match arranger.selection() { - Selection::Track(_) => Selection::Track(index), - Selection::TrackClip { track, scene } => Selection::TrackClip { - track: index, scene: *scene - }, - _ => *arranger.selection() + 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 }; Ok(Some(Self::TrackDelete { index })) } @@ -102,42 +115,37 @@ impl ArrangementCommand { //TODO:Ok(Some(Self::TrackAdd ( index, track: Some(deleted_track) }) } fn midi_in (arranger: &mut Arrangement, input: MidiInputCommand) -> Perhaps { - todo!("delegate"); - Ok(None) + todo!("delegate") } 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"); - Ok(None) + 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"); - Ok(None) + todo!("delegate") } fn device_add (arranger: &mut Arrangement, i: usize) -> Perhaps { - todo!("delegate"); + arranger.device_add(i); Ok(None) } fn scene (arranger: &mut Arrangement, scene: SceneCommand) -> Perhaps { - todo!("delegate"); - Ok(None) + todo!("delegate") } fn scene_add (arranger: &mut Arrangement) -> Perhaps { let index = arranger.scene_add(None, None)?.0; - *arranger.selection_mut() = match arranger.selection() { - Selection::Scene(_) => Selection::Scene(index), - Selection::TrackClip { track, scene } => Selection::TrackClip { - track: *track, - scene: index - }, - _ => *arranger.selection() + 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 }; Ok(None) // TODO } @@ -192,20 +200,117 @@ impl ArrangementCommand { } } -impl<'state> Context<'state, MidiInputCommand> for Arrangement { - fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { +impl<'state> Context<'state, TrackCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { Context::get(&self, iter) } } -impl<'state> Context<'state, MidiOutputCommand> for Arrangement { - fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { +#[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 { Context::get(&self, iter) } } -impl<'state> Context<'state, DeviceCommand> for Arrangement { - fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { +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_clip.rs b/crates/device/src/arranger/arranger_clip.rs index 98dd7ba7..b5acd217 100644 --- a/crates/device/src/arranger/arranger_clip.rs +++ b/crates/device/src/arranger/arranger_clip.rs @@ -1,36 +1,100 @@ 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 + 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() + } + } + +} + +#[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 a1d680fc..e12e73d1 100644 --- a/crates/device/src/arranger/arranger_model.rs +++ b/crates/device/src/arranger/arranger_model.rs @@ -6,10 +6,6 @@ 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 @@ -33,53 +29,51 @@ pub struct Arrangement { /// Scroll offset of scenes pub scene_scroll: usize, /// Selected UI element - pub selection: Selection, + 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, - /// Contains all clips in arrangement - pub pool: Pool, + /// Jack client handle + pub jack: Jack, } -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); +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); impl Arrangement { /// Width of display - pub fn w (&self) -> u16 { + pub(crate) fn w (&self) -> u16 { self.size.w() as u16 } /// Width allocated for sidebar. - pub fn w_sidebar (&self, is_editing: bool) -> u16 { + 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 fn w_tracks (&self) -> u16 { - self.tracks_with_sizes(&self.selection(), None).last() - .map(|(_, _, _, x)|x as u16).unwrap_or(0) + 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 fn w_tracks_area (&self, is_editing: bool) -> u16 { + 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 fn h (&self) -> u16 { + pub(crate) fn h (&self) -> u16 { self.size.h() as u16 } /// Height available to display track headers. - pub fn h_tracks_area (&self) -> u16 { + 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 fn h_scenes_area (&self) -> u16 { + pub(crate) fn h_scenes_area (&self) -> u16 { //15 self.h().saturating_sub( self.h_inputs() + @@ -89,90 +83,34 @@ impl Arrangement { ) } /// Height taken by all scenes. - pub fn h_scenes (&self, is_editing: bool) -> u16 { + 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, - self.selection().track(), - self.selection().scene(), + selected_track, + selected_scene ) .last() .map(|(_, _, _, y)|y as u16).unwrap_or(0) } /// Height taken by all inputs. - pub fn h_inputs (&self) -> u16 { + 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 fn h_outputs (&self) -> u16 { + 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 fn h_devices (&self) -> u16 { + pub(crate) fn h_devices (&self) -> u16 { 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 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 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 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.get_track()?.sampler(0) - } - /// Get the first sampler of the active track - pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.get_track_mut()?.sampler_mut(0) - } } diff --git a/crates/device/src/arranger/arranger_scene.rs b/crates/device/src/arranger/arranger_scene.rs index 209d5397..a217d43e 100644 --- a/crates/device/src/arranger/arranger_scene.rs +++ b/crates/device/src/arranger/arranger_scene.rs @@ -1,42 +1,10 @@ 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<&Scene> { - Has::>::get(self).as_ref() + fn scene (&self) -> &Option { + Has::>::get(self) } 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 db9536ba..2ef03033 100644 --- a/crates/device/src/arranger/arranger_select.rs +++ b/crates/device/src/arranger/arranger_select.rs @@ -11,14 +11,37 @@ 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)) + } + 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 { - #[default] - /// Nothing is selected - Nothing, /// The whole mix is selected - Mix, + #[default] Mix, /// A MIDI input is selected. Input(usize), /// A MIDI output is selected. diff --git a/crates/device/src/arranger/arranger_track.rs b/crates/device/src/arranger/arranger_track.rs index 20b1157d..41cb5bf1 100644 --- a/crates/device/src/arranger/arranger_track.rs +++ b/crates/device/src/arranger/arranger_track.rs @@ -1,57 +1,5 @@ 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 @@ -149,27 +97,197 @@ impl Track { } } -pub trait HasTrack { - fn track (&self) -> Option<&Track>; - fn track_mut (&mut self) -> Option<&mut 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>> 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> + Send + Sync> HasTrack for T {} -impl> + Has> HasTrack for T { +pub trait HasTrack: Has> + Send + Sync { fn track (&self) -> Option<&Track> { - let index = Has::::get(self).track()?; - Has::>::get(self).get(index) + Has::>::get(self).as_ref() } fn track_mut (&mut self) -> Option<&mut Track> { - let index = Has::::get(self).track()?; - Has::>::get_mut(self).get_mut(index) + 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 09a3d273..e69de29b 100644 --- a/crates/device/src/arranger/arranger_tracks.rs +++ b/crates/device/src/arranger/arranger_tracks.rs @@ -1,146 +0,0 @@ -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: &Selection, - editor_width: Option - ) -> impl TracksSizes<'_> { - let mut x = 0; - let active_track = if let Some(width) = editor_width { - 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.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)) - }) - } -} - -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])) - } -} diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs index 7e5edc22..e28d3362 100644 --- a/crates/device/src/arranger/arranger_view.rs +++ b/crates/device/src/arranger/arranger_view.rs @@ -1,6 +1,6 @@ use crate::*; -pub struct ArrangerView<'a> { +pub(crate) struct ArrangerView<'a> { pub arrangement: &'a Arrangement, pub is_editing: bool, @@ -35,6 +35,7 @@ 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); @@ -53,7 +54,7 @@ impl<'a> ArrangerView<'a> { outputs_count: arrangement.midi_outs.len(), scenes_height: h_scenes_area, - scene_selected: arrangement.selection().scene(), + 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 { @@ -64,7 +65,7 @@ impl<'a> ArrangerView<'a> { tracks_height: h_tracks_area, track_count: arrangement.tracks.len(), - track_selected: arrangement.selection().track(), + 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, @@ -85,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)))))) } } @@ -149,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: &'a Option) -> impl Content + 'a { + 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 { @@ -159,77 +160,88 @@ impl<'a> ArrangerView<'a> { track_selected, is_editing, .. } = self; - let selection = Has::::get(self.arrangement); - let selected_track = selection.track(); - let selected_scene = selection.scene(); + 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( - 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] - }))) - })) + ||scenes_with_scene_colors(), + scene_header)) .middle(*width_mid, per_track( *width_mid, ||self.tracks_with_sizes_scrolled(), - 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>, _|{ + |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) @@ -271,29 +283,3 @@ 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/browser/browser_model.rs b/crates/device/src/browser/browser_model.rs index 213d4276..c06caff6 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, Default)] +#[derive(Debug, Clone)] pub struct Browser { pub cwd: PathBuf, pub dirs: Vec<(OsString, String)>, diff --git a/crates/device/src/clock/clock_model.rs b/crates/device/src/clock/clock_model.rs index c961233a..e9016264 100644 --- a/crates/device/src/clock/clock_model.rs +++ b/crates/device/src/clock/clock_model.rs @@ -177,25 +177,4 @@ 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/clock/clock_view.rs b/crates/device/src/clock/clock_view.rs index 92e778eb..ab2acc3e 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: Option>, + sel: Arc, sr: Arc>, buf: Arc>, lat: Arc>, ) -> impl Content { let theme = ItemTheme::G[96]; Tui::bg(Black, row!(Bsp::a( - Fill::xy(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))), + Fill::xy(Align::w(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 9d8d31f6..648e70c5 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<&MidiEditor>; - fn editor_mut (&mut self) -> Option<&mut MidiEditor>; + fn editor (&self) -> &Option; + fn editor_mut (&mut self) -> &Option; fn is_editing (&self) -> bool { true } fn editor_w (&self) -> usize { 0 } fn editor_h (&self) -> usize { 0 } @@ -117,12 +117,17 @@ pub trait HasEditor { is_editing = $e3:expr; }) => { impl HasEditor for $Struct { - fn editor (&$self) -> Option<&MidiEditor> { $e0.as_ref() } - fn editor_mut (&mut $self) -> Option<&mut MidiEditor> { $e0.as_mut() } + fn editor (&$self) -> &Option { &$e0 } + fn editor_mut (&mut $self) -> &Option { &mut $e0 } 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 } + } + }; } diff --git a/crates/device/src/sequencer/seq_audio.rs b/crates/device/src/sequencer/seq_audio.rs index 060d833b..c96b7cdc 100644 --- a/crates/device/src/sequencer/seq_audio.rs +++ b/crates/device/src/sequencer/seq_audio.rs @@ -1,45 +1,82 @@ 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 Sequencer { +impl Audio for PlayerAudio<'_, T> { fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - if self.clock().is_rolling() { - self.process_rolling(scope) - } else { - self.process_stopped(scope) + 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 !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 } } -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 +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(); } - 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 monitoring (&self) -> bool; + + fn monitoring_mut (&mut self) -> &mut bool; + + fn toggle_monitor (&mut self) { + *self.monitoring_mut() = !self.monitoring(); } - fn process_monitoring (&mut self, scope: &ProcessScope) { - let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat + + 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(); let monitoring = self.monitoring(); - for input in self.midi_ins.iter() { - for (sample, event, bytes) in input.parsed(scope) { + for input in self.midi_ins_mut().iter() { + for (sample, event, bytes) in parse_midi_input(input.port().iter(scope)) { if let LiveEvent::Midi { message, .. } = event { if monitoring { - self.midi_buf[sample].push(bytes.to_vec()); + midi_buf[sample].push(bytes.to_vec()); } // FIXME: don't lock on every event! update_keys(&mut notes_in.write().unwrap(), &message); @@ -47,143 +84,28 @@ impl Sequencer { } } } - /// 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) { + + fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { if self.monitoring() { - self.monitor(scope); + self.monitor(scope, midi_buf); } - if let Some((started, ref clip)) = self.play_clip.clone() { - self.record_clip(scope, started, clip); + 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((_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>>, + scope: &ProcessScope, + started: Moment, + clip: &Option>>, + _midi_buf: &mut Vec>> ) { if let Some(clip) = clip { let sample0 = scope.last_frame_time() as usize; @@ -191,7 +113,7 @@ pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { 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)) { @@ -211,4 +133,173 @@ pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { 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 5524dc04..a4dd982d 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) -> &Sequencer { + fn sequencer (&self) -> &impl MidiSequencer { self.get() } - fn sequencer_mut (&mut self) -> &mut Sequencer { + fn sequencer_mut (&mut self) -> &mut impl MidiSequencer { self.get_mut() } } pub trait HasSequencer { - fn sequencer (&self) -> &Sequencer; - fn sequencer_mut (&mut self) -> &mut Sequencer; + fn sequencer (&self) -> &impl MidiSequencer; + fn sequencer_mut (&mut self) -> &mut impl MidiSequencer; } /// Contains state for playing a clip @@ -41,8 +41,6 @@ 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 { @@ -57,7 +55,6 @@ 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![], @@ -83,10 +80,14 @@ 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() }) } } @@ -105,9 +106,12 @@ has!(Clock: |self: Sequencer|self.clock); has!(Vec: |self:Sequencer| self.midi_ins); has!(Vec: |self:Sequencer| self.midi_outs); -impl MidiMonitor for Sequencer { - fn notes_in (&self) -> &Arc> { - &self.notes_in +impl MidiRecorder for Sequencer { + fn recording (&self) -> bool { + self.recording + } + fn recording_mut (&mut self) -> &mut bool { + &mut self.recording } fn monitoring (&self) -> bool { self.monitoring @@ -115,21 +119,21 @@ impl MidiMonitor 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 8d0d852d..85136cb9 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,3 +233,5 @@ 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 8f891e4c..e9bcf677 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -18,16 +18,6 @@ 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 { @@ -37,14 +27,6 @@ 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; diff --git a/crates/engine/src/midi.rs b/crates/engine/src/midi.rs index c3588d1c..e6a656b0 100644 --- a/crates/engine/src/midi.rs +++ b/crates/engine/src/midi.rs @@ -9,9 +9,17 @@ pub use ::midly::{ live::*, }; -mod midi_in; pub use self::midi_in::*; -mod midi_out; pub use self::midi_out::*; -mod midi_hold; pub use self::midi_hold::*; +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; }, + _ => {} + } +} /// 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 deleted file mode 100644 index 0717e90c..00000000 --- a/crates/engine/src/midi/midi_hold.rs +++ /dev/null @@ -1,10 +0,0 @@ -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 027025d0..27cbd76e 100644 --- a/crates/engine/src/midi/midi_in.rs +++ b/crates/engine/src/midi/midi_in.rs @@ -1,11 +1,5 @@ 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 514f6487..89a22296 100644 --- a/crates/engine/src/midi/midi_out.rs +++ b/crates/engine/src/midi/midi_out.rs @@ -1,151 +1,5 @@ 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 { } @@ -175,11 +29,6 @@ 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 a9619ab9..bad20f50 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit a9619ab9cea40aed3ec30483f4a7db03ca7fec9d +Subproject commit bad20f5037dc22d572a8381840fab871ce65f565