diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index 18c6d61f..ad036139 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -6,27 +6,21 @@ pub struct App { pub jack: Jack, /// Port handles pub ports: std::collections::BTreeMap>, - /// Source of time - pub clock: Clock, /// Display size pub size: Measure, /// Performance counter pub perf: PerfModel, // View and input definition - pub config: Configuration, - /// Undo history - pub history: Vec, - // Dialog overlay - pub dialog: Option, - /// Browses external resources, such as directories - pub browser: Option, - /// Contains all clips in the project - pub pool: Option, - /// Contains the currently edited MIDI clip - pub editor: Option, + pub config: Configuration, /// Contains the currently edited musical arrangement - pub arranger: Arrangement, + pub project: Arrangement, + /// Undo history + pub history: Vec, + // Dialog overlay + pub dialog: Option, + /// Contains the currently edited MIDI clip + pub editor: Option, // Cache of formatted strings pub view_cache: Arc>, @@ -34,11 +28,12 @@ pub struct App { pub color: ItemTheme, } -has!(Option: |self: App|self.arrangement.selected); -has!(Vec: |self: App|self.arrangement.midi_ins); -has!(Vec: |self: App|self.arrangement.midi_outs); -has!(Vec: |self: App|self.arrangement.scenes); -has!(Vec: |self: App|self.arrangement.tracks); +has!(Option: |self: App|self.project.selected); +has!(Vec: |self: App|self.project.midi_ins); +has!(Vec: |self: App|self.project.midi_outs); +has!(Vec: |self: App|self.project.scenes); +has!(Vec: |self: App|self.project.tracks); +has!(Clock: |self: App|self.project.clock); impl App { pub fn toggle_dialog (&mut self, dialog: Option) { @@ -67,11 +62,85 @@ impl App { } pub(crate) fn device_add (&mut self, index: usize) -> Usually<()> { match index { - 0 => self.arrangement.device_add_sampler(), - 1 => self.arrangement.device_add_lv2(), + 0 => self.device_add_sampler(), + 1 => self.device_add_lv2(), _ => unreachable!(), } } + fn device_add_lv2 (&mut self) -> Usually<()> { + todo!(); + Ok(()) + } + fn device_add_sampler (&mut self, jack: &Jack) -> Usually { + let name = jack.with_client(|c|c.name().to_string()); + let midi = self.track().expect("no active track").sequencer.midi_outs[0].name(); + let sampler = if let Ok(sampler) = Sampler::new( + jack, + &format!("{}/Sampler", Has::::get(self).name), + &[PortConnect::exact(format!("{name}:{midi}"))], + &[&[], &[]], + &[&[], &[]] + ) { + self.dialog = None; + Device::Sampler(sampler) + } else { + self.dialog = Some(Dialog::Message(Message::FailedToAddDevice)); + return Err("failed to add device".into()) + }; + Has::::get_mut(self).expect("no active track").devices.push(sampler); + Ok(()) + } + // Create new clip in pool when entering empty cell + fn clip_auto_create (&mut self) -> Option>> { + if let Some(pool) = Has::>::get(self) + && let Some(Selection::TrackClip { track, scene }) = self.get() + && let Some(scene) = Has::>::get_mut(self).get_mut(*scene) + && let Some(slot) = scene.clips.get_mut(*track) + && slot.is_none() + && let Some(track) = Has::>::get_mut(self).get_mut(*track) + { + let (index, mut clip) = pool.add_new_clip(); + // autocolor: new clip colors from scene and track color + let color = track.color.base.mix(scene.color.base, 0.5); + clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); + if let Some(ref mut editor) = Has::>::get_mut(self) { + editor.set_clip(Some(&clip)); + } + *slot = Some(clip.clone()); + Some(clip) + } else { + None + } + } +} + +#[cfg(all(feature = "pool", feature = "editor"))] +pub trait AutoRemove: + Has> + + Has> + + Has> + + Has> + + Has> + + Send + Sync +{ + // Remove clip from arrangement when exiting empty clip editor + fn clip_auto_remove (&mut self) { + if let Some(ref mut pool) = Has::>::get(self) + && let Some(Selection::TrackClip { track, scene }) = self.get() + && let Some(scene) = Has::>::get_mut(self).get_mut(*scene) + && let Some(slot) = scene.clips.get_mut(*track) + && let Some(clip) = slot.as_mut() + { + let mut swapped = None; + if clip.read().unwrap().count_midi_messages() == 0 { + std::mem::swap(&mut swapped, slot); + } + if let Some(clip) = swapped { + pool.delete_clip(&clip.read().unwrap()); + } + } + } +} } /// Various possible dialog overlays @@ -81,8 +150,8 @@ pub enum Dialog { Menu, Device(usize), Message(Message), - Save, - Load, + Save(Browser), + Load(Browser), Options, } @@ -234,27 +303,4 @@ impl App { 0 } } - fn device_add_lv2 (&mut self) -> Usually<()> { - todo!(); - Ok(()) - } - fn device_add_sampler (&mut self, jack: &Jack) -> Usually { - let name = jack.with_client(|c|c.name().to_string()); - let midi = self.track().expect("no active track").sequencer.midi_outs[0].name(); - let sampler = if let Ok(sampler) = Sampler::new( - jack, - &format!("{}/Sampler", Has::::get(self).name), - &[PortConnect::exact(format!("{name}:{midi}"))], - &[&[], &[]], - &[&[], &[]] - ) { - self.dialog = None; - Device::Sampler(sampler) - } else { - self.dialog = Some(Dialog::Message(Message::FailedToAddDevice)); - return Err("failed to add device".into()) - }; - Has::::get_mut(self).expect("no active track").devices.push(sampler); - Ok(()) - } } diff --git a/crates/device/src/arranger/arranger_api.rs b/crates/device/src/arranger/arranger_api.rs index bd6d4721..9034baca 100644 --- a/crates/device/src/arranger/arranger_api.rs +++ b/crates/device/src/arranger/arranger_api.rs @@ -6,6 +6,7 @@ impl Arrangement { fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } + fn _todo_opt_selection_ (&self) -> Option { todo!() } } #[tengri_proc::command(Arrangement)] @@ -200,117 +201,20 @@ impl ArrangementCommand { } } -impl<'state> Context<'state, TrackCommand> for Arrangement { - fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { +impl<'state> Context<'state, MidiInputCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { Context::get(&self, iter) } } -#[tengri_proc::expose] -impl Track { - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } -} - -#[tengri_proc::command(Track)] -impl TrackCommand { - fn set_name (track: &mut Track, mut name: Arc) -> Perhaps { - std::mem::swap(&mut name, &mut track.name); - Ok(Some(Self::SetName { name })) - } - fn set_color (track: &mut Track, mut color: ItemTheme) -> Perhaps { - std::mem::swap(&mut color, &mut track.color); - Ok(Some(Self::SetColor { color })) - } - fn set_mute (track: &mut Track, value: Option) -> Perhaps { - todo!() - } - fn set_solo (track: &mut Track, value: Option) -> Perhaps { - todo!() - } - fn set_rec (track: &mut Track, value: Option) -> Perhaps { - let current = track.sequencer.recording; - let value = value.unwrap_or(!current); - Ok((value != current).then_some(Self::SetRec { value: Some(current) })) - } - fn set_mon (track: &mut Track, value: Option) -> Perhaps { - let current = track.sequencer.monitoring; - let value = value.unwrap_or(!current); - Ok((value != current).then_some(Self::SetRec { value: Some(current) })) - } - fn set_size (track: &mut Track, size: usize) -> Perhaps { - todo!() - } - fn set_zoom (track: &mut Track, zoom: usize) -> Perhaps { - todo!() - } - fn stop (track: &mut Track) -> Perhaps { - track.sequencer.enqueue_next(None); - Ok(None) - } -} - -impl<'state> Context<'state, SceneCommand> for Arrangement { - fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { +impl<'state> Context<'state, MidiOutputCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { Context::get(&self, iter) } } -impl<'state> Context<'state, ClipCommand> for Arrangement { - fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { +impl<'state> Context<'state, DeviceCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { Context::get(&self, iter) } } - -#[tengri_proc::expose] -impl Scene { - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } -} - -#[tengri_proc::command(Scene)] -impl SceneCommand { - fn set_name (scene: &mut Scene, mut name: Arc) -> Perhaps { - std::mem::swap(&mut scene.name, &mut name); - Ok(Some(Self::SetName { name })) - } - fn set_color (scene: &mut Scene, mut color: ItemTheme) -> Perhaps { - std::mem::swap(&mut scene.color, &mut color); - Ok(Some(Self::SetColor { color })) - } - fn set_size (scene: &mut Scene, size: usize) -> Perhaps { - todo!() - } - fn set_zoom (scene: &mut Scene, zoom: usize) -> Perhaps { - todo!() - } -} - -#[tengri_proc::expose] -impl MidiClip { - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_bool_stub_ (&self) -> bool { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } - fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } -} - -#[tengri_proc::command(MidiClip)] -impl ClipCommand { - fn set_color (clip: &mut MidiClip, color: Option) -> Perhaps { - //(SetColor [t: usize, s: usize, c: ItemTheme] - //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); - //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) - todo!() - } - fn set_loop (clip: &mut MidiClip, looping: Option) -> Perhaps { - //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) - //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) - todo!() - } -} diff --git a/crates/device/src/arranger/arranger_clip.rs b/crates/device/src/arranger/arranger_clip.rs index b5acd217..b0ad719a 100644 --- a/crates/device/src/arranger/arranger_clip.rs +++ b/crates/device/src/arranger/arranger_clip.rs @@ -1,5 +1,36 @@ use crate::*; +impl<'state> Context<'state, ClipCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { + Context::get(&self, iter) + } +} + +#[tengri_proc::expose] +impl MidiClip { + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_bool_stub_ (&self) -> bool { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } +} + +#[tengri_proc::command(MidiClip)] +impl ClipCommand { + fn set_color (clip: &mut MidiClip, color: Option) -> Perhaps { + //(SetColor [t: usize, s: usize, c: ItemTheme] + //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); + //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) + todo!() + } + fn set_loop (clip: &mut MidiClip, looping: Option) -> Perhaps { + //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) + //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) + todo!() + } +} + impl Arrangement { /// Put a clip in a slot @@ -26,7 +57,7 @@ impl Arrangement { /// Get the active clip pub(crate) fn clip (&self) -> Option>> { - self.scene()?.clips.get(self.selected().track()?)?.clone() + self.scene()?.clips.get(self.selected.track()?)?.clone() } /// Toggle looping for the active clip @@ -37,64 +68,3 @@ impl Arrangement { } } - -#[cfg(all(feature = "pool", feature = "editor"))] -pub trait AutoCreate: - Has> + - Has> + - Has> + - Has> + - Has> + - Send + Sync -{ - // Create new clip in pool when entering empty cell - fn clip_auto_create (&mut self) -> Option>> { - if let Some(pool) = Has::>::get(self) - && let Some(Selection::TrackClip { track, scene }) = self.get() - && let Some(scene) = Has::>::get_mut(self).get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && slot.is_none() - && let Some(track) = Has::>::get_mut(self).get_mut(track) - { - let (index, mut clip) = pool.add_new_clip(); - // autocolor: new clip colors from scene and track color - let color = track.color.base.mix(scene.color.base, 0.5); - clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); - if let Some(ref mut editor) = Has::>::get_mut(self) { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip.clone()); - Some(clip) - } else { - None - } - } -} - -#[cfg(all(feature = "pool", feature = "editor"))] -pub trait AutoRemove: - Has> + - Has> + - Has> + - Has> + - Has> + - Send + Sync -{ - // Remove clip from arrangement when exiting empty clip editor - fn clip_auto_remove (&mut self) { - if let Some(ref mut pool) = Has::>::get(self) - && let Some(Selection::TrackClip { track, scene }) = self.get() - && let Some(scene) = Has::>::get_mut(self).get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && let Some(clip) = slot.as_mut() - { - let mut swapped = None; - if clip.read().unwrap().count_midi_messages() == 0 { - std::mem::swap(&mut swapped, slot); - } - if let Some(clip) = swapped { - pool.delete_clip(&clip.read().unwrap()); - } - } - } -} diff --git a/crates/device/src/arranger/arranger_model.rs b/crates/device/src/arranger/arranger_model.rs index e12e73d1..d2f3246c 100644 --- a/crates/device/src/arranger/arranger_model.rs +++ b/crates/device/src/arranger/arranger_model.rs @@ -6,6 +6,10 @@ pub struct Arrangement { pub name: Arc, /// Base color. pub color: ItemTheme, + /// Jack client handle + pub jack: Jack, + /// Source of time + pub clock: Clock, /// List of global midi inputs pub midi_ins: Vec, /// List of global midi outputs @@ -35,8 +39,8 @@ pub struct Arrangement { pub arranger: Arc>, /// Display size pub size: Measure, - /// Jack client handle - pub jack: Jack, + /// Contains all clips in arrangement + pub pool: Pool, } has!(Option: |self: Arrangement|self.selected); @@ -45,6 +49,7 @@ has!(Vec: |self: Arrangement|self.midi_outs); has!(Vec: |self: Arrangement|self.scenes); has!(Vec: |self: Arrangement|self.tracks); has!(Jack: |self: Arrangement|self.jack); +has!(Clock: |self: Arrangement|self.clock); impl Arrangement { /// Width of display @@ -57,7 +62,8 @@ impl Arrangement { } /// Width taken by all tracks. pub(crate) fn w_tracks (&self) -> u16 { - self.tracks_with_sizes().last().map(|(_, _, _, x)|x as u16).unwrap_or(0) + self.tracks_with_sizes(&self.selected, None).last() + .map(|(_, _, _, x)|x as u16).unwrap_or(0) } /// Width available to display tracks. pub(crate) fn w_tracks_area (&self, is_editing: bool) -> u16 { @@ -114,3 +120,15 @@ impl Arrangement { //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } } + +#[cfg(feature = "sampler")] +impl Arrangement { + /// Get the first sampler of the active track + pub fn sampler (&self) -> Option<&Sampler> { + self.track().map(|t|t.sampler(0)).flatten() + } + /// Get the first sampler of the active track + pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { + self.track_mut().map(|t|t.sampler_mut(0)).flatten() + } +} diff --git a/crates/device/src/arranger/arranger_scene.rs b/crates/device/src/arranger/arranger_scene.rs index a217d43e..209d5397 100644 --- a/crates/device/src/arranger/arranger_scene.rs +++ b/crates/device/src/arranger/arranger_scene.rs @@ -1,10 +1,42 @@ use crate::*; +impl<'state> Context<'state, SceneCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { + Context::get(&self, iter) + } +} + +#[tengri_proc::expose] +impl Scene { + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } +} + +#[tengri_proc::command(Scene)] +impl SceneCommand { + fn set_name (scene: &mut Scene, mut name: Arc) -> Perhaps { + std::mem::swap(&mut scene.name, &mut name); + Ok(Some(Self::SetName { name })) + } + fn set_color (scene: &mut Scene, mut color: ItemTheme) -> Perhaps { + std::mem::swap(&mut scene.color, &mut color); + Ok(Some(Self::SetColor { color })) + } + fn set_size (scene: &mut Scene, size: usize) -> Perhaps { + todo!() + } + fn set_zoom (scene: &mut Scene, zoom: usize) -> Perhaps { + todo!() + } +} + impl> + Send + Sync> HasScene for T {} pub trait HasScene: Has> + Send + Sync { - fn scene (&self) -> &Option { - Has::>::get(self) + fn scene (&self) -> Option<&Scene> { + Has::>::get(self).as_ref() } fn scene_mut (&mut self) -> &mut Option { Has::>::get_mut(self) diff --git a/crates/device/src/arranger/arranger_select.rs b/crates/device/src/arranger/arranger_select.rs index 2ef03033..8330faf2 100644 --- a/crates/device/src/arranger/arranger_select.rs +++ b/crates/device/src/arranger/arranger_select.rs @@ -13,9 +13,9 @@ pub trait HasSelection: Has { impl Has> for Arrangement { fn get (&self) -> &Option { - Has::>::get(self) - .and_then(|selection|selection.track()) - .and_then(|index|Has::>::get(self).get(index)) + let selection: Selection = Has::>::get(self)?; + let index: Option = selection.track()?; + let track: Track = Has::>::get(self).get(index)?; } fn get_mut (&mut self) -> &mut Option { Has::>::get(self) @@ -33,7 +33,7 @@ impl Has> for Arrangement { fn get_mut (&mut self) -> &mut Option { Has::>::get(self) .and_then(|selection|selection.track()) - .and_then(|index|Has::>::get_mut(self).get_mut(index)) + .and_then(|index|Has::>::get_mut(self).get_mut(index).as_deref()) } } diff --git a/crates/device/src/arranger/arranger_track.rs b/crates/device/src/arranger/arranger_track.rs index 41cb5bf1..26449380 100644 --- a/crates/device/src/arranger/arranger_track.rs +++ b/crates/device/src/arranger/arranger_track.rs @@ -1,5 +1,57 @@ use crate::*; +impl<'state> Context<'state, TrackCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { + Context::get(&self, iter) + } +} + +#[tengri_proc::expose] +impl Track { + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } +} + +#[tengri_proc::command(Track)] +impl TrackCommand { + fn set_name (track: &mut Track, mut name: Arc) -> Perhaps { + std::mem::swap(&mut name, &mut track.name); + Ok(Some(Self::SetName { name })) + } + fn set_color (track: &mut Track, mut color: ItemTheme) -> Perhaps { + std::mem::swap(&mut color, &mut track.color); + Ok(Some(Self::SetColor { color })) + } + fn set_mute (track: &mut Track, value: Option) -> Perhaps { + todo!() + } + fn set_solo (track: &mut Track, value: Option) -> Perhaps { + todo!() + } + fn set_rec (track: &mut Track, value: Option) -> Perhaps { + let current = track.sequencer.recording; + let value = value.unwrap_or(!current); + Ok((value != current).then_some(Self::SetRec { value: Some(current) })) + } + fn set_mon (track: &mut Track, value: Option) -> Perhaps { + let current = track.sequencer.monitoring; + let value = value.unwrap_or(!current); + Ok((value != current).then_some(Self::SetRec { value: Some(current) })) + } + fn set_size (track: &mut Track, size: usize) -> Perhaps { + todo!() + } + fn set_zoom (track: &mut Track, zoom: usize) -> Perhaps { + todo!() + } + fn stop (track: &mut Track) -> Perhaps { + track.sequencer.enqueue_next(None); + Ok(None) + } +} + #[derive(Debug, Default)] pub struct Track { /// Name of track @@ -97,77 +149,6 @@ impl Track { } } -impl> + Send + Sync> HasTracks for T {} - -pub trait HasTracks: Has> + Send + Sync { - fn tracks (&self) -> &Vec { - Has::>::get(self) - } - fn tracks_mut (&mut self) -> &mut Vec { - Has::>::get_mut(self) - } - /// Run audio callbacks for every track and every device - fn tracks_render (&mut self, client: &Client, scope: &ProcessScope) -> Control { - for track in self.tracks_mut().iter_mut() { - if Control::Quit == PlayerAudio( - track.sequencer_mut(), &mut self.note_buf, &mut self.midi_buf - ).process(client, scope) { - return Control::Quit - } - for device in track.devices.iter_mut() { - if Control::Quit == DeviceAudio(device).process(client, scope) { - return Control::Quit - } - } - } - Control::Continue - } - fn track_longest_name (&self) -> usize { - self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) - } - /// Stop all playing clips - fn tracks_stop_all (&mut self) { - for track in self.tracks_mut().iter_mut() { - track.sequencer.enqueue_next(None); - } - } - /// Stop all playing clips - fn tracks_launch (&mut self, clips: Option>>>>) { - if let Some(clips) = clips { - for (clip, track) in clips.iter().zip(self.tracks_mut()) { - track.sequencer.enqueue_next(clip); - } - } else { - for track in self.tracks_mut().iter_mut() { - track.sequencer.enqueue_next(None); - } - } - } - /// Iterate over tracks with their corresponding sizes. - fn tracks_with_sizes ( - &self, - selection: &Option, - editor_width: Option - ) -> impl TracksSizes<'_> { - let mut x = 0; - let active_track = if let Some(width) = editor_width && let Some(selection) = selection { - selection.track() - } else { - None - }; - self.tracks().iter().enumerate().map(move |(index, track)|{ - let width = active_track - .and_then(|_|editor_width) - .unwrap_or(track.width.max(8)); - let data = (index, track, x, x + width); - x += width + Self::TRACK_SPACING; - data - }) - } - /// Spacing between tracks. - const TRACK_SPACING: usize = 0; -} - impl> + Send + Sync> HasTrack for T {} pub trait HasTrack: Has> + Send + Sync { @@ -178,116 +159,3 @@ pub trait HasTrack: Has> + Send + Sync { Has::>::get_mut(self).as_mut() } } - -impl<'a> ArrangerView<'a> { - pub(crate) fn tracks_with_sizes_scrolled (&'a self) - -> impl TracksSizes<'a> - { - self.arrangement - .tracks_with_sizes(&self.arrangement.selected, self.is_editing.then_some(20/*FIXME*/)) - .map_while(move|(t, track, x1, x2)|{ - (self.width_mid > x2 as u16).then_some((t, track, x1, x2)) - }) - } -} - -pub(crate) fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( - width: u16, - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Content + 'a { - Align::x(Tui::bg(Reset, Map::new(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - map_east(x1 as u16, width, Fixed::x(width, Tui::fg_bg( - track.color.lightest.rgb, - track.color.base.rgb, - callback(index, track))))}))) -} - -pub(crate) fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( - width: u16, - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Content + 'a { - per_track_top( - width, - tracks, - move|index, track|Fill::y(Align::y(callback(index, track))) - ) -} - -impl AddTrack for T {} - -pub trait AddTrack: HasTracks + HasScenes + HasClock + HasJack { - /// Add multiple tracks - fn tracks_add ( - &mut self, - count: usize, - width: Option, - mins: &[PortConnect], - mouts: &[PortConnect], - ) -> Usually<()> { - let jack = self.jack().clone(); - let track_color_1 = ItemColor::random(); - let track_color_2 = ItemColor::random(); - for i in 0..count { - let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); - let mut track = self.track_add(None, Some(color), mins, mouts)?.1; - if let Some(width) = width { - track.width = width; - } - } - Ok(()) - } - - /// Add a track - fn track_add ( - &mut self, - name: Option<&str>, - color: Option, - mins: &[PortConnect], - mouts: &[PortConnect], - ) -> Usually<(usize, &mut Track)> { - self.track_last += 1; - let name: Arc = name.map_or_else( - ||format!("Track{:02}", self.track_last).into(), - |x|x.to_string().into() - ); - let mut track = Track { - width: (name.len() + 2).max(12), - color: color.unwrap_or_else(ItemTheme::random), - sequencer: Sequencer::new( - &format!("{name}"), - self.jack(), - Some(self.clock()), - None, - mins, - mouts - )?, - name, - ..Default::default() - }; - self.tracks_mut().push(track); - let len = self.tracks().len(); - let index = len - 1; - for scene in self.scenes_mut().iter_mut() { - while scene.clips.len() < len { - scene.clips.push(None); - } - } - Ok((index, &mut self.tracks_mut()[index])) - } -} - -#[cfg(feature = "sampler")] -impl Arrangement { - /// Get the first sampler of the active track - pub fn sampler (&self) -> Option<&Sampler> { - self.track().map(|t|t.sampler(0)).flatten() - } - /// Get the first sampler of the active track - pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.track_mut().map(|t|t.sampler_mut(0)).flatten() - } -} diff --git a/crates/device/src/arranger/arranger_tracks.rs b/crates/device/src/arranger/arranger_tracks.rs index e69de29b..7863a9e7 100644 --- a/crates/device/src/arranger/arranger_tracks.rs +++ b/crates/device/src/arranger/arranger_tracks.rs @@ -0,0 +1,145 @@ +use crate::*; + +impl> + Send + Sync> HasTracks for T {} + +pub trait HasTracks: Has> + Send + Sync { + fn tracks (&self) -> &Vec { + Has::>::get(self) + } + fn tracks_mut (&mut self) -> &mut Vec { + Has::>::get_mut(self) + } + /// Run audio callbacks for every track and every device + fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { + for track in self.tracks_mut().iter_mut() { + if Control::Quit == Audio::process(&mut track.sequencer, client, scope) { + return Control::Quit + } + for device in track.devices.iter_mut() { + if Control::Quit == DeviceAudio(device).process(client, scope) { + return Control::Quit + } + } + } + Control::Continue + } + fn track_longest_name (&self) -> usize { + self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) + } + /// Stop all playing clips + fn tracks_stop_all (&mut self) { + for track in self.tracks_mut().iter_mut() { + track.sequencer.enqueue_next(None); + } + } + /// Stop all playing clips + fn tracks_launch (&mut self, clips: Option>>>>) { + if let Some(clips) = clips { + for (clip, track) in clips.iter().zip(self.tracks_mut()) { + track.sequencer.enqueue_next(clip.as_ref()); + } + } else { + for track in self.tracks_mut().iter_mut() { + track.sequencer.enqueue_next(None); + } + } + } + /// Iterate over tracks with their corresponding sizes. + fn tracks_with_sizes ( + &self, + selection: &Option, + editor_width: Option + ) -> impl TracksSizes<'_> { + let mut x = 0; + let active_track = if let Some(width) = editor_width && let Some(selection) = selection { + selection.track() + } else { + None + }; + self.tracks().iter().enumerate().map(move |(index, track)|{ + let width = active_track + .and_then(|_|editor_width) + .unwrap_or(track.width.max(8)); + let data = (index, track, x, x + width); + x += width + Self::TRACK_SPACING; + data + }) + } + /// Spacing between tracks. + const TRACK_SPACING: usize = 0; +} + +impl<'a> ArrangerView<'a> { + pub(crate) fn tracks_with_sizes_scrolled (&'a self) + -> impl TracksSizes<'a> + { + self.arrangement + .tracks_with_sizes(&self.arrangement.selected, self.is_editing.then_some(20/*FIXME*/)) + .map_while(move|(t, track, x1, x2)|{ + (self.width_mid > x2 as u16).then_some((t, track, x1, x2)) + }) + } +} + +impl AddTrack for T {} + +pub trait AddTrack: HasTracks + HasScenes + HasClock + HasJack { + /// Add multiple tracks + fn tracks_add ( + &mut self, + count: usize, + width: Option, + mins: &[PortConnect], + mouts: &[PortConnect], + ) -> Usually<()> { + let jack = self.jack().clone(); + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..count { + let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); + let mut track = self.track_add(None, Some(color), mins, mouts)?.1; + if let Some(width) = width { + track.width = width; + } + } + Ok(()) + } + + /// Add a track + fn track_add ( + &mut self, + name: Option<&str>, + color: Option, + mins: &[PortConnect], + mouts: &[PortConnect], + ) -> Usually<(usize, &mut Track)> { + self.track_last += 1; + let name: Arc = name.map_or_else( + ||format!("Track{:02}", self.track_last).into(), + |x|x.to_string().into() + ); + let mut track = Track { + width: (name.len() + 2).max(12), + color: color.unwrap_or_else(ItemTheme::random), + sequencer: Sequencer::new( + &format!("{name}"), + self.jack(), + Some(self.clock()), + None, + mins, + mouts + )?, + name, + ..Default::default() + }; + self.tracks_mut().push(track); + let len = self.tracks().len(); + let index = len - 1; + for scene in self.scenes_mut().iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); + } + } + Ok((index, &mut self.tracks_mut()[index])) + } +} diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs index e28d3362..e6213432 100644 --- a/crates/device/src/arranger/arranger_view.rs +++ b/crates/device/src/arranger/arranger_view.rs @@ -283,3 +283,29 @@ impl<'a> ArrangerView<'a> { } } + +pub(crate) fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( + width: u16, + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Content + 'a { + Align::x(Tui::bg(Reset, Map::new(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + let width = (x2 - x1) as u16; + map_east(x1 as u16, width, Fixed::x(width, Tui::fg_bg( + track.color.lightest.rgb, + track.color.base.rgb, + callback(index, track))))}))) +} + +pub(crate) fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( + width: u16, + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Content + 'a { + per_track_top( + width, + tracks, + move|index, track|Fill::y(Align::y(callback(index, track))) + ) +} diff --git a/crates/device/src/clock/clock_model.rs b/crates/device/src/clock/clock_model.rs index e9016264..c961233a 100644 --- a/crates/device/src/clock/clock_model.rs +++ b/crates/device/src/clock/clock_model.rs @@ -177,4 +177,25 @@ impl Clock { pub fn next_launch_instant (&self) -> Moment { Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) } + + /// Get index of first sample to populate. + /// + /// Greater than 0 means that the first pulse of the clip + /// falls somewhere in the middle of the chunk. + pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ + (scope.last_frame_time() as usize).saturating_sub( + started.sample.get() as usize + + self.started.read().unwrap().as_ref().unwrap().sample.get() as usize + ) + } + + // Get iterator that emits sample paired with pulse. + // + // * Sample: index into output buffer at which to write MIDI event + // * Pulse: index into clip from which to take the MIDI event + // + // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. + pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> TicksIterator { + self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) + } } diff --git a/crates/device/src/sequencer/seq_audio.rs b/crates/device/src/sequencer/seq_audio.rs index c96b7cdc..060d833b 100644 --- a/crates/device/src/sequencer/seq_audio.rs +++ b/crates/device/src/sequencer/seq_audio.rs @@ -1,82 +1,45 @@ use crate::*; -/// Hosts the JACK callback for a single MIDI sequencer -pub struct PlayerAudio<'a, T: MidiSequencer>( - /// Player - pub &'a mut T, - /// Note buffer - pub &'a mut Vec, - /// Note chunk buffer - pub &'a mut Vec>>, -); - /// JACK process callback for a sequencer's clip sequencer/recorder. -impl Audio for PlayerAudio<'_, T> { +impl Audio for Sequencer { fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - let model = &mut self.0; - let note_buf = &mut self.1; - let midi_buf = &mut self.2; - // Clear output buffer(s) - model.clear(scope, midi_buf, false); - // Write chunk of clip to output, handle switchover - if model.play(scope, note_buf, midi_buf) { - model.switchover(scope, note_buf, midi_buf); + if self.clock().is_rolling() { + self.process_rolling(scope) + } else { + self.process_stopped(scope) } - if !model.midi_ins().is_empty() { - if model.recording() || model.monitoring() { - // Record and/or monitor input - model.record(scope, midi_buf) - } else if model.midi_outs().is_empty() && model.monitoring() { - // Monitor input to output - model.monitor(scope, midi_buf) - } - } - // Write to output port(s) - model.write(scope, midi_buf); - Control::Continue } } -pub trait MidiSequencer: MidiRecorder + MidiPlayer + Send + Sync {} - -impl MidiSequencer for Sequencer {} - -pub trait MidiRecorder: HasClock + HasPlayClip + HasMidiIns { - fn notes_in (&self) -> &Arc>; - - fn recording (&self) -> bool; - - fn recording_mut (&mut self) -> &mut bool; - - fn toggle_record (&mut self) { - *self.recording_mut() = !self.recording(); +impl Sequencer { + fn process_rolling (&mut self, scope: &ProcessScope) -> Control { + self.process_clear(scope, false); + // Write chunk of clip to output, handle switchover + if self.process_playback(scope) { + self.process_switchover(scope); + } + // Monitor input to output + self.process_monitoring(scope); + // Record and/or monitor input + self.process_recording(scope); + // Emit contents of MIDI buffers to JACK MIDI output ports. + self.midi_outs_emit(scope); + Control::Continue } - - fn monitoring (&self) -> bool; - - fn monitoring_mut (&mut self) -> &mut bool; - - fn toggle_monitor (&mut self) { - *self.monitoring_mut() = !self.monitoring(); + fn process_stopped (&mut self, scope: &ProcessScope) -> Control { + if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { + self.process_monitoring(scope) + } + Control::Continue } - - fn overdub (&self) -> bool; - - fn overdub_mut (&mut self) -> &mut bool; - - fn toggle_overdub (&mut self) { - *self.overdub_mut() = !self.overdub(); - } - - fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { - // For highlighting keys and note repeat - let notes_in = self.notes_in().clone(); + fn process_monitoring (&mut self, scope: &ProcessScope) { + let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat let monitoring = self.monitoring(); - for input in self.midi_ins_mut().iter() { - for (sample, event, bytes) in parse_midi_input(input.port().iter(scope)) { + for input in self.midi_ins.iter() { + for (sample, event, bytes) in input.parsed(scope) { if let LiveEvent::Midi { message, .. } = event { if monitoring { - midi_buf[sample].push(bytes.to_vec()); + self.midi_buf[sample].push(bytes.to_vec()); } // FIXME: don't lock on every event! update_keys(&mut notes_in.write().unwrap(), &message); @@ -84,28 +47,143 @@ pub trait MidiRecorder: HasClock + HasPlayClip + HasMidiIns { } } } - - fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); + for frame in &mut self.midi_buf_mut()[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(self.midi_buf_mut()); + } + for port in self.midi_outs_mut().iter_mut() { + // Clear output buffer(s) + port.buffer_clear(scope, false); + } + } + fn process_recording (&mut self, scope: &ProcessScope) { if self.monitoring() { - self.monitor(scope, midi_buf); + self.monitor(scope); } - if !self.clock().is_rolling() { - return - } - if let Some((started, ref clip)) = self.play_clip().clone() { - self.record_clip(scope, started, clip, midi_buf); + if let Some((started, ref clip)) = self.play_clip.clone() { + self.record_clip(scope, started, clip); } if let Some((_start_at, _clip)) = &self.next_clip() { self.record_next(); } } + fn process_playback (&mut self, scope: &ProcessScope) -> bool { + // If a clip is playing, write a chunk of MIDI events from it to the output buffer. + // If no clip is playing, prepare for switchover immediately. + if let Some((started, clip)) = &self.play_clip { + // Length of clip, to repeat or stop on end. + let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); + // Index of first sample to populate. + let offset = self.clock().get_sample_offset(scope, &started); + // Write MIDI events from clip at sample offsets corresponding to pulses. + for (sample, pulse) in self.clock().get_pulses(scope, offset) { + // If a next clip is enqueued, and we're past the end of the current one, + // break the loop here (FIXME count pulse correctly) + let past_end = if clip.is_some() { pulse >= length } else { true }; + // Is it time for switchover? + if self.next_clip().is_some() && past_end { + return true + } + // If there's a currently playing clip, output notes from it to buffer: + if let Some(ref clip) = clip { + // Source clip from which the MIDI events will be taken. + let clip = clip.read().unwrap(); + // Clip with zero length is not processed + if clip.length > 0 { + // Current pulse index in source clip + let pulse = pulse % clip.length; + // Output each MIDI event from clip at appropriate frames of output buffer: + for message in clip.notes[pulse].iter() { + for port in self.midi_outs.iter_mut() { + port.buffer_write(sample, LiveEvent::Midi { + channel: 0.into(), /* TODO */ + message: *message + }); + } + } + } + } + } + false + } else { + true + } + } + /// Handle switchover from current to next playing clip. + fn process_switchover (&mut self, scope: &ProcessScope) { + let midi_buf = self.midi_buf_mut(); + let sample0 = scope.last_frame_time() as usize; + //let samples = scope.n_frames() as usize; + if let Some((start_at, clip)) = &self.next_clip() { + let start = start_at.sample.get() as usize; + let sample = self.clock().started.read().unwrap() + .as_ref().unwrap().sample.get() as usize; + // If it's time to switch to the next clip: + if start <= sample0.saturating_sub(sample) { + // Samples elapsed since clip was supposed to start + let _skipped = sample0 - start; + // Switch over to enqueued clip + let started = Moment::from_sample(self.clock().timebase(), start as f64); + // Launch enqueued clip + *self.play_clip_mut() = Some((started, clip.clone())); + // Unset enqueuement (TODO: where to implement looping?) + *self.next_clip_mut() = None; + // Fill in remaining ticks of chunk from next clip. + self.process_playback(scope); + } + } + } +} + +pub trait HasMidiBuffers { + fn note_buf_mut (&mut self) -> &mut Vec; + fn midi_buf_mut (&mut self) -> &mut Vec>>; +} + +impl HasMidiBuffers for Sequencer { + fn note_buf_mut (&mut self) -> &mut Vec { + &mut self.note_buf + } + fn midi_buf_mut (&mut self) -> &mut Vec>> { + &mut self.midi_buf + } +} + +pub trait MidiMonitor: HasMidiIns + HasMidiBuffers { + fn notes_in (&self) -> &Arc>; + fn monitoring (&self) -> bool; + fn monitoring_mut (&mut self) -> &mut bool; + fn toggle_monitor (&mut self) { + *self.monitoring_mut() = !self.monitoring(); + } + fn monitor (&mut self, scope: &ProcessScope) { + } +} + +pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { + fn recording (&self) -> bool; + fn recording_mut (&mut self) -> &mut bool; + fn toggle_record (&mut self) { + *self.recording_mut() = !self.recording(); + } + + fn overdub (&self) -> bool; + fn overdub_mut (&mut self) -> &mut bool; + fn toggle_overdub (&mut self) { + *self.overdub_mut() = !self.overdub(); + } fn record_clip ( &mut self, - scope: &ProcessScope, - started: Moment, - clip: &Option>>, - _midi_buf: &mut Vec>> + scope: &ProcessScope, + started: Moment, + clip: &Option>>, ) { if let Some(clip) = clip { let sample0 = scope.last_frame_time() as usize; @@ -113,7 +191,7 @@ pub trait MidiRecorder: HasClock + HasPlayClip + HasMidiIns { let _recording = self.recording(); let timebase = self.clock().timebase().clone(); let quant = self.clock().quant.get(); - let mut clip = clip.write().unwrap(); + let mut clip = clip.write().unwrap(); let length = clip.length; for input in self.midi_ins_mut().iter() { for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) { @@ -133,173 +211,4 @@ pub trait MidiRecorder: HasClock + HasPlayClip + HasMidiIns { fn record_next (&mut self) { // TODO switch to next clip and record into it } - -} - -pub trait MidiPlayer: HasPlayClip + HasClock + HasMidiOuts { - - fn notes_out (&self) -> &Arc>; - - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - fn clear ( - &mut self, scope: &ProcessScope, out: &mut [Vec>], reset: bool - ) { - let n_frames = (scope.n_frames() as usize).min(out.len()); - for frame in &mut out[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(out); - } - } - - /// Output notes from clip to MIDI output ports. - fn play ( - &mut self, scope: &ProcessScope, note_buf: &mut Vec, out: &mut [Vec>] - ) -> bool { - if !self.clock().is_rolling() { - return false - } - // If a clip is playing, write a chunk of MIDI events from it to the output buffer. - // If no clip is playing, prepare for switchover immediately. - if let Some((started, clip)) = self.play_clip() { - self.play_chunk(scope, note_buf, out, started, clip) - } else { - true - } - } - - fn play_chunk ( - &self, - scope: &ProcessScope, - note_buf: &mut Vec, - out: &mut [Vec>], - started: &Moment, - clip: &Option>> - ) -> bool { - // Index of first sample to populate. - let offset = self.get_sample_offset(scope, started); - // Notes active during current chunk. - let notes = &mut self.notes_out().write().unwrap(); - // Length of clip. - let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); - // Write MIDI events from clip at sample offsets corresponding to pulses. - for (sample, pulse) in self.get_pulses(scope, offset) { - // If a next clip is enqueued, and we're past the end of the current one, - // break the loop here (FIXME count pulse correctly) - let past_end = if clip.is_some() { pulse >= length } else { true }; - // Is it time for switchover? - if self.next_clip().is_some() && past_end { - return true - } - // If there's a currently playing clip, output notes from it to buffer: - if let Some(ref clip) = clip { - Self::play_pulse(clip, pulse, sample, note_buf, out, notes) - } - } - false - } - - /// Get index of first sample to populate. - /// - /// Greater than 0 means that the first pulse of the clip - /// falls somewhere in the middle of the chunk. - fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ - (scope.last_frame_time() as usize).saturating_sub( - started.sample.get() as usize + - self.clock().started.read().unwrap().as_ref().unwrap().sample.get() as usize - ) - } - - // Get iterator that emits sample paired with pulse. - // - // * Sample: index into output buffer at which to write MIDI event - // * Pulse: index into clip from which to take the MIDI event - // - // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. - fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> TicksIterator { - self.clock().timebase().pulses_between_samples( - offset, offset + scope.n_frames() as usize) - } - - /// Handle switchover from current to next playing clip. - fn switchover ( - &mut self, scope: &ProcessScope, note_buf: &mut Vec, out: &mut [Vec>] - ) { - if !self.clock().is_rolling() { - return - } - let sample0 = scope.last_frame_time() as usize; - //let samples = scope.n_frames() as usize; - if let Some((start_at, clip)) = &self.next_clip() { - let start = start_at.sample.get() as usize; - let sample = self.clock().started.read().unwrap() - .as_ref().unwrap().sample.get() as usize; - // If it's time to switch to the next clip: - if start <= sample0.saturating_sub(sample) { - // Samples elapsed since clip was supposed to start - let _skipped = sample0 - start; - // Switch over to enqueued clip - let started = Moment::from_sample(self.clock().timebase(), start as f64); - // Launch enqueued clip - *self.play_clip_mut() = Some((started, clip.clone())); - // Unset enqueuement (TODO: where to implement looping?) - *self.next_clip_mut() = None; - // Fill in remaining ticks of chunk from next clip. - self.play(scope, note_buf, out); - } - } - } - - fn play_pulse ( - clip: &RwLock, - pulse: usize, - sample: usize, - note_buf: &mut Vec, - out: &mut [Vec>], - notes: &mut [bool;128] - ) { - // Source clip from which the MIDI events will be taken. - let clip = clip.read().unwrap(); - // Clip with zero length is not processed - if clip.length > 0 { - // Current pulse index in source clip - let pulse = pulse % clip.length; - // Output each MIDI event from clip at appropriate frames of output buffer: - for message in clip.notes[pulse].iter() { - // Clear output buffer for this MIDI event. - note_buf.clear(); - // TODO: support MIDI channels other than CH1. - let channel = 0.into(); - // Serialize MIDI event into message buffer. - LiveEvent::Midi { channel, message: *message } - .write(note_buf) - .unwrap(); - // Append serialized message to output buffer. - out[sample].push(note_buf.clone()); - // Update the list of currently held notes. - update_keys(&mut*notes, message); - } - } - } - - /// Write a chunk of MIDI data from the output buffer to all assigned output ports. - fn write (&mut self, scope: &ProcessScope, out: &[Vec>]) { - let samples = scope.n_frames() as usize; - for port in self.midi_outs_mut().iter_mut() { - Self::write_port(&mut port.port_mut().writer(scope), samples, out) - } - } - - /// Write a chunk of MIDI data from the output buffer to an output port. - fn write_port (writer: &mut MidiWriter, samples: usize, out: &[Vec>]) { - for (time, events) in out.iter().enumerate().take(samples) { - for bytes in events.iter() { - writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ - panic!("Failed to write MIDI data: {bytes:?}"); - }); - } - } - } } diff --git a/crates/device/src/sequencer/seq_model.rs b/crates/device/src/sequencer/seq_model.rs index a4dd982d..5524dc04 100644 --- a/crates/device/src/sequencer/seq_model.rs +++ b/crates/device/src/sequencer/seq_model.rs @@ -2,17 +2,17 @@ use crate::*; impl> HasSequencer for T { - fn sequencer (&self) -> &impl MidiSequencer { + fn sequencer (&self) -> &Sequencer { self.get() } - fn sequencer_mut (&mut self) -> &mut impl MidiSequencer { + fn sequencer_mut (&mut self) -> &mut Sequencer { self.get_mut() } } pub trait HasSequencer { - fn sequencer (&self) -> &impl MidiSequencer; - fn sequencer_mut (&mut self) -> &mut impl MidiSequencer; + fn sequencer (&self) -> &Sequencer; + fn sequencer_mut (&mut self) -> &mut Sequencer; } /// Contains state for playing a clip @@ -41,6 +41,8 @@ pub struct Sequencer { pub notes_out: Arc>, /// MIDI output buffer pub note_buf: Vec, + /// MIDI output buffer + pub midi_buf: Vec>>, } impl Default for Sequencer { @@ -55,6 +57,7 @@ impl Default for Sequencer { notes_in: RwLock::new([false;128]).into(), notes_out: RwLock::new([false;128]).into(), note_buf: vec![0;8], + midi_buf: vec![], reset: true, midi_ins: vec![], @@ -80,14 +83,10 @@ impl Sequencer { midi_outs: vec![JackMidiOut::new(jack, format!("{}/M", name.as_ref()), midi_to)?, ], play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), clock, - note_buf: vec![0;8], reset: true, - recording: false, - monitoring: false, - overdub: false, - next_clip: None, notes_in: RwLock::new([false;128]).into(), notes_out: RwLock::new([false;128]).into(), + ..Default::default() }) } } @@ -106,12 +105,9 @@ has!(Clock: |self: Sequencer|self.clock); has!(Vec: |self:Sequencer| self.midi_ins); has!(Vec: |self:Sequencer| self.midi_outs); -impl MidiRecorder for Sequencer { - fn recording (&self) -> bool { - self.recording - } - fn recording_mut (&mut self) -> &mut bool { - &mut self.recording +impl MidiMonitor for Sequencer { + fn notes_in (&self) -> &Arc> { + &self.notes_in } fn monitoring (&self) -> bool { self.monitoring @@ -119,21 +115,21 @@ impl MidiRecorder for Sequencer { fn monitoring_mut (&mut self) -> &mut bool { &mut self.monitoring } +} + +impl MidiRecord for Sequencer { + fn recording (&self) -> bool { + self.recording + } + fn recording_mut (&mut self) -> &mut bool { + &mut self.recording + } fn overdub (&self) -> bool { self.overdub } fn overdub_mut (&mut self) -> &mut bool { &mut self.overdub } - fn notes_in (&self) -> &Arc> { - &self.notes_in - } -} - -impl MidiPlayer for Sequencer { - fn notes_out (&self) -> &Arc> { - &self.notes_out - } } impl HasPlayClip for Sequencer { diff --git a/crates/engine/src/jack/jack_port.rs b/crates/engine/src/jack/jack_port.rs index 85136cb9..8d0d852d 100644 --- a/crates/engine/src/jack/jack_port.rs +++ b/crates/engine/src/jack/jack_port.rs @@ -189,7 +189,7 @@ macro_rules! impl_port { type Pair = $Pair; fn port (&self) -> &Port<$Spec> { &self.port } } - impl JackPortConnect<&str> for $Name { + impl JackPortConnect<&str> for $Name { fn connect_to (&self, to: &str) -> Usually { self.with_client(|c|if let Some(ref port) = c.port_by_name(to.as_ref()) { self.connect_to(port) @@ -233,5 +233,3 @@ impl_port!(JackAudioIn: AudioIn -> AudioOut |j, n|j.register_port::(n impl_port!(JackAudioOut: AudioOut -> AudioIn |j, n|j.register_port::(n)); impl_port!(JackMidiIn: MidiIn -> MidiOut |j, n|j.register_port::(n)); - -impl_port!(JackMidiOut: MidiOut -> MidiIn |j, n|j.register_port::(n)); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index e9bcf677..55b8d382 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -18,6 +18,16 @@ pub trait Has: Send + Sync { fn get_mut (&mut self) -> &mut T; } +pub trait MaybeHas: Send + Sync { + fn get (&self) -> Option<&T>; +} + +impl>> MaybeHas for U { + fn get (&self) -> Option<&T> { + Has::>::get(self).as_ref() + } +} + #[macro_export] macro_rules! has { ($T:ty: |$self:ident : $S:ty| $x:expr) => { impl Has<$T> for $S { diff --git a/crates/engine/src/midi.rs b/crates/engine/src/midi.rs index e6a656b0..c3588d1c 100644 --- a/crates/engine/src/midi.rs +++ b/crates/engine/src/midi.rs @@ -9,17 +9,9 @@ pub use ::midly::{ live::*, }; -mod midi_in; pub use self::midi_in::*; -mod midi_out; pub use self::midi_out::*; - -/// Update notes_in array -pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { - match message { - MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } - MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, - _ => {} - } -} +mod midi_in; pub use self::midi_in::*; +mod midi_out; pub use self::midi_out::*; +mod midi_hold; pub use self::midi_hold::*; /// Return boxed iterator of MIDI events pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>) -> Box, &'a [u8])> + 'a> { diff --git a/crates/engine/src/midi/midi_hold.rs b/crates/engine/src/midi/midi_hold.rs new file mode 100644 index 00000000..0717e90c --- /dev/null +++ b/crates/engine/src/midi/midi_hold.rs @@ -0,0 +1,10 @@ +use crate::*; + +/// Update notes_in array +pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { + match message { + MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } + MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, + _ => {} + } +} diff --git a/crates/engine/src/midi/midi_in.rs b/crates/engine/src/midi/midi_in.rs index 27cbd76e..027025d0 100644 --- a/crates/engine/src/midi/midi_in.rs +++ b/crates/engine/src/midi/midi_in.rs @@ -1,5 +1,11 @@ use crate::*; +impl JackMidiIn { + pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { + parse_midi_input(self.port().iter(scope)) + } +} + #[tengri_proc::command(JackMidiIn)] impl MidiInputCommand { } diff --git a/crates/engine/src/midi/midi_out.rs b/crates/engine/src/midi/midi_out.rs index 89a22296..514f6487 100644 --- a/crates/engine/src/midi/midi_out.rs +++ b/crates/engine/src/midi/midi_out.rs @@ -1,5 +1,151 @@ use crate::*; +#[derive(Debug)] pub struct JackMidiOut { + /// Handle to JACK client, for receiving reconnect events. + jack: Jack, + /// Port name + name: Arc, + /// Port handle. + port: Port, + /// List of ports to connect to. + conn: Vec, + /// List of currently held notes. + held: Arc>, + /// Buffer + note_buffer: Vec, + /// Buffer + output_buffer: Vec>>, +} + +has!(Jack: |self: JackMidiOut|self.jack); + +impl JackMidiOut { + pub fn new (jack: &Jack, name: impl AsRef, connect: &[PortConnect]) + -> Usually + { + let jack = jack.clone(); + let port = jack.register_port::(name.as_ref())?; + let name = name.as_ref().into(); + let conn = connect.to_vec(); + let port = Self { + jack, + port, + name, + conn, + held: Arc::new([false;128].into()), + note_buffer: vec![0;8], + output_buffer: vec![vec![];65536], + }; + port.connect_to_matching()?; + Ok(port) + } + pub fn name (&self) -> &Arc { + &self.name + } + pub fn port (&self) -> &Port { + &self.port + } + pub fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + pub fn into_port (self) -> Port { + self.port + } + pub fn close (self) -> Usually<()> { + let Self { jack, port, .. } = self; + Ok(jack.with_client(|client|client.unregister_port(port))?) + } + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); + for frame in &mut self.output_buffer[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(&mut self.output_buffer); + } + } + /// Write a note to the output buffer + pub fn buffer_write <'a> ( + &'a mut self, + sample: usize, + event: LiveEvent, + ) { + self.note_buffer.fill(0); + event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); + self.output_buffer[sample].push(self.note_buffer.clone()); + // Update the list of currently held notes. + if let LiveEvent::Midi { ref message, .. } = event { + update_keys(&mut*self.held.write().unwrap(), message); + } + } + /// Write a chunk of MIDI data from the output buffer to the output port. + pub fn buffer_emit (&mut self, scope: &ProcessScope) { + let samples = scope.n_frames() as usize; + let mut writer = self.port.writer(scope); + for (time, events) in self.output_buffer.iter().enumerate().take(samples) { + for bytes in events.iter() { + writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ + panic!("Failed to write MIDI data: {bytes:?}"); + }); + } + } + } +} + +impl AsRef> for JackMidiOut { + fn as_ref (&self) -> &Port { + &self.port + } +} + +impl JackPort for JackMidiOut { + type Port = MidiOut; + type Pair = MidiIn; + fn port (&self) -> &Port { &self.port } +} + +impl JackPortConnect<&str> for JackMidiOut { + fn connect_to (&self, to: &str) -> Usually { + self.with_client(|c|if let Some(ref port) = c.port_by_name(to.as_ref()) { + self.connect_to(port) + } else { + Ok(Missing) + }) + } +} + +impl JackPortConnect<&Port> for JackMidiOut { + fn connect_to (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) { + Connected + } else if let Ok(_) = c.connect_ports(port, &self.port) { + Connected + } else { + Mismatch + })) + } +} + +impl JackPortConnect<&Port> for JackMidiOut { + fn connect_to (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) { + Connected + } else if let Ok(_) = c.connect_ports(port, &self.port) { + Connected + } else { + Mismatch + })) + } +} + +impl JackPortAutoconnect for JackMidiOut { + fn conn (&self) -> &[PortConnect] { + &self.conn + } +} + #[tengri_proc::command(JackMidiOut)] impl MidiOutputCommand { } @@ -29,6 +175,11 @@ pub trait HasMidiOuts { data }) } + fn midi_outs_emit (&mut self, scope: &ProcessScope) { + for port in self.midi_outs_mut().iter_mut() { + port.buffer_emit(scope) + } + } } /// Trail for thing that may gain new MIDI ports. diff --git a/deps/tengri b/deps/tengri index bad20f50..a9619ab9 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit bad20f5037dc22d572a8381840fab871ce65f565 +Subproject commit a9619ab9cea40aed3ec30483f4a7db03ca7fec9d