From c78b2dc9dea6e75dc33c4b7937f02b585c57030e Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 10 May 2025 18:47:58 +0300 Subject: [PATCH 01/15] device: add DeviceAudio dispatcher --- crates/app/src/audio.rs | 30 ++++++++++++++------------ crates/device/src/device.rs | 43 +++++++++++++++++++++++++++++++++++++ crates/device/src/lib.rs | 23 +++----------------- 3 files changed, 62 insertions(+), 34 deletions(-) create mode 100644 crates/device/src/device.rs diff --git a/crates/app/src/audio.rs b/crates/app/src/audio.rs index 5bdf1ffc..b6db6a44 100644 --- a/crates/app/src/audio.rs +++ b/crates/app/src/audio.rs @@ -2,26 +2,20 @@ use crate::*; impl HasJack for App { fn jack (&self) -> &Jack { &self.jack } } audio!( |self: App, client, scope|{ + // Start profiling cycle let t0 = self.perf.get_t0(); + // Update transport clock self.clock().update_from_scope(scope).unwrap(); - // Collect MIDI input (TODO preallocate) + + // Collect MIDI input (TODO preallocate large buffers) let midi_in = self.midi_ins.iter() .map(|port|port.port().iter(scope) .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) .collect::>()) .collect::>(); - // Update standalone MIDI sequencer - //if let Some(player) = self.player.as_mut() { - //if Control::Quit == PlayerAudio( - //player, - //&mut self.note_buf, - //&mut self.midi_buf, - //).process(client, scope) { - //return Control::Quit - //} - //} + // Update standalone sampler //if let Some(sampler) = self.sampler.as_mut() { //if Control::Quit == SamplerAudio(sampler).process(client, scope) { @@ -35,6 +29,7 @@ audio!( //} //} //} + // TODO move these to editor and sampler?: //for port in midi_in.iter() { //for event in port.iter() { @@ -58,14 +53,21 @@ audio!( //} //} //} - // Update track sequencers + + // Update track sequencers and devices for track in self.tracks.iter_mut() { - if PlayerAudio( + if Control::Quit == PlayerAudio( track.player_mut(), &mut self.note_buf, &mut self.midi_buf - ).process(client, scope) == Control::Quit { + ).process(client, scope) { return Control::Quit } + for device in track.devices.iter_mut() { + if Control::Quit == DeviceAudio(device).process(client, scope) { + return Control::Quit + } + } } + // End profiling cycle self.perf.update_from_jack_scope(t0, scope); Control::Continue diff --git a/crates/device/src/device.rs b/crates/device/src/device.rs new file mode 100644 index 00000000..74d76488 --- /dev/null +++ b/crates/device/src/device.rs @@ -0,0 +1,43 @@ +use crate::*; + +#[derive(Debug)] +pub enum Device { + #[cfg(feature = "sequencer")] Sequencer(MidiPlayer), + #[cfg(feature = "sampler")] Sampler(Sampler), + #[cfg(feature = "lv2")] Lv2(Lv2), // TODO + #[cfg(feature = "vst2")] Vst2, // TODO + #[cfg(feature = "vst3")] Vst3, // TODO + #[cfg(feature = "clap")] Clap, // TODO + #[cfg(feature = "sf2")] Sf2, // TODO +} + +impl Device { + pub fn name (&self) -> &str { + match self { + Self::Sampler(sampler) => sampler.name.as_ref(), + _ => todo!(), + } + } +} + +pub struct DeviceAudio<'a>(pub &'a mut Device); + +audio!(|self: DeviceAudio<'a>, client, scope|{ + use Device::*; + match self.0 { + #[cfg(feature = "sequencer")] Sequencer(sequencer) => + { Control::Continue /* TODO */ }, + #[cfg(feature = "sampler")] Sampler(sampler) => + SamplerAudio(sampler).process(client, scope), + #[cfg(feature = "lv2")] Lv2(lv2) => + { todo!() }, // TODO + #[cfg(feature = "vst2")] Vst2 => + { todo!() }, // TODO + #[cfg(feature = "vst3")] Vst3 => + { todo!() }, // TODO + #[cfg(feature = "clap")] Clap => + { todo!() }, // TODO + #[cfg(feature = "sf2")] Sf2 => + { todo!() }, // TODO + } +}); diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 64d99c31..16f34bf2 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -15,6 +15,9 @@ pub(crate) use ::tek_engine::midi::{u7, LiveEvent, MidiMessage}; pub(crate) use ::tek_engine::jack::{Control, ProcessScope, MidiWriter, RawMidi}; pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}}; +mod device; +pub use self::device::*; + #[cfg(feature = "clock")] mod clock; #[cfg(feature = "clock")] pub use self::clock::*; @@ -38,23 +41,3 @@ pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line} #[cfg(feature = "clap")] mod clap; #[cfg(feature = "clap")] pub use self::clap::*; - -#[derive(Debug)] -pub enum Device { - #[cfg(feature = "sequencer")] Sequencer(MidiPlayer), - #[cfg(feature = "sampler")] Sampler(Sampler), - #[cfg(feature = "lv2")] Lv2(Lv2), // TODO - #[cfg(feature = "vst2")] Vst2, // TODO - #[cfg(feature = "vst3")] Vst3, // TODO - #[cfg(feature = "clap")] Clap, // TODO - #[cfg(feature = "sf2")] Sf2, // TODO -} - -impl Device { - pub fn name (&self) -> &str { - match self { - Self::Sampler(sampler) => sampler.name.as_ref(), - _ => todo!(), - } - } -} From c5586c3a35480ffe6efdaa84effea52f9ffde7dd Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 10 May 2025 18:49:03 +0300 Subject: [PATCH 02/15] simplify track construction --- crates/app/src/model/track.rs | 33 ++++++++++----------------------- crates/cli/tek.rs | 14 ++++++++------ 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/crates/app/src/model/track.rs b/crates/app/src/model/track.rs index 5fa9d941..6cf670b5 100644 --- a/crates/app/src/model/track.rs +++ b/crates/app/src/model/track.rs @@ -23,36 +23,23 @@ has_player!(|self: Track|self.player); impl Track { pub const MIN_WIDTH: usize = 9; - /// Create a new track containing a sequencer. - pub fn new_sequencer () -> Self { - let mut track = Self::default(); - track.devices.push(Device::Sequencer(MidiPlayer::default())); - track + /// Create a new track with only the default [MidiPlayer]. + pub fn new () -> Self { + Self::default() } - /// Create a new track containing a sequencer and sampler. - pub fn new_groovebox ( + /// Create a new track connecting the [MidiPlayer] to a [Sampler]. + pub fn new_with_sampler ( jack: &Jack, midi_from: &[PortConnect], audio_from: &[&[PortConnect];2], audio_to: &[&[PortConnect];2], ) -> Usually { let mut track = Self::new_sequencer(); - track.devices.push(Device::Sampler( - Sampler::new(jack, &"sampler", midi_from, audio_from, audio_to)? - )); - Ok(track) - } - /// Create a new track containing a sampler. - pub fn new_sampler ( - jack: &Jack, - midi_from: &[PortConnect], - audio_from: &[&[PortConnect];2], - audio_to: &[&[PortConnect];2], - ) -> Usually { - let mut track = Self::default(); - track.devices.push(Device::Sampler( - Sampler::new(jack, &"sampler", midi_from, audio_from, audio_to)? - )); + let name = jack.with_client(|c|c.name().to_string()); + let midi = track.player.midi_outs[0].name(); + let port = PortConnect::exact(format!("{name}:{midi}")); + let sampler = Sampler::new(jack, &"sampler", &[port], audio_from, audio_to)?; + track.devices.push(Device::Sampler(sampler)); Ok(track) } pub fn width_inc (&mut self) { diff --git a/crates/cli/tek.rs b/crates/cli/tek.rs index ff5baa49..e6434a26 100644 --- a/crates/cli/tek.rs +++ b/crates/cli/tek.rs @@ -136,13 +136,15 @@ impl Cli { }, tracks: match mode { LaunchMode::Sequencer => vec![ - Track::new_sequencer() + Track::new() ], - LaunchMode::Groovebox => vec![ - Track::new_groovebox(jack, midi_froms.as_slice(), audio_froms, audio_tos)? - ], - LaunchMode::Sampler => vec![ - Track::new_sampler(jack, midi_froms.as_slice(), audio_froms, audio_tos)? + LaunchMode::Groovebox | LaunchMode::Sampler => vec![ + Track::new_with_sampler( + jack, + midi_froms.as_slice(), + audio_froms, + audio_tos, + )? ], _ => vec![] }, From 5fab1af13893babb81c321a43226d72926b03a9e Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 10 May 2025 19:08:22 +0300 Subject: [PATCH 03/15] MidiPlayer -> Sequencer; connect sequencer to sampler in groovebox mode --- crates/app/src/api.rs | 4 +- crates/app/src/audio.rs | 2 +- crates/app/src/model.rs | 16 ++-- crates/app/src/model/scene.rs | 2 +- crates/app/src/model/track.rs | 104 ++++++++++++++--------- crates/app/src/view.rs | 10 +-- crates/cli/tek.rs | 13 ++- crates/device/src/device.rs | 2 +- crates/device/src/sequencer.rs | 4 +- crates/device/src/sequencer/seq_model.rs | 44 +++++----- 10 files changed, 120 insertions(+), 81 deletions(-) diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index 39d7f317..4e4ae4b0 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -469,7 +469,7 @@ impl<'state> Context<'state, SamplerCommand> for App { Ok(None) } fn stop (app: &mut App, index: usize) -> Perhaps { - app.tracks[index].player.enqueue_next(None); + app.tracks[index].sequencer.enqueue_next(None); Ok(None) } fn add (app: &mut App) -> Perhaps { @@ -535,7 +535,7 @@ impl<'state> Context<'state, SamplerCommand> for App { } fn enqueue (app: &mut App, a: usize, b: usize) -> Perhaps { //(Enqueue [t: usize, s: usize] - //cmd!(app.tracks[t].player.enqueue_next(app.scenes[s].clips[t].as_ref()))) + //cmd!(app.tracks[t].sequencer.enqueue_next(app.scenes[s].clips[t].as_ref()))) //("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap()))) todo!() } diff --git a/crates/app/src/audio.rs b/crates/app/src/audio.rs index b6db6a44..0081d093 100644 --- a/crates/app/src/audio.rs +++ b/crates/app/src/audio.rs @@ -57,7 +57,7 @@ audio!( // Update track sequencers and devices for track in self.tracks.iter_mut() { if Control::Quit == PlayerAudio( - track.player_mut(), &mut self.note_buf, &mut self.midi_buf + track.sequencer_mut(), &mut self.note_buf, &mut self.midi_buf ).process(client, scope) { return Control::Quit } diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index a18943f4..41c4c875 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -102,7 +102,7 @@ impl App { let mut track = Track { width: (name.len() + 2).max(12), color: color.unwrap_or_else(ItemTheme::random), - player: MidiPlayer::new( + sequencer: Sequencer::new( &format!("{name}"), self.jack(), Some(self.clock()), @@ -141,7 +141,7 @@ impl App { let exists = self.tracks().get(index).is_some(); if exists { let track = self.tracks_mut().remove(index); - let Track { player: MidiPlayer { midi_ins, midi_outs, .. }, .. } = track; + let Track { sequencer: Sequencer { midi_ins, midi_outs, .. }, .. } = track; for port in midi_ins.into_iter() { port.close()?; } @@ -196,7 +196,7 @@ impl App { /// Enqueue clips from a scene across all tracks pub fn scene_enqueue (&mut self, scene: usize) { for track in 0..self.tracks.len() { - self.tracks[track].player.enqueue_next(self.scenes[scene].clips[track].as_ref()); + self.tracks[track].sequencer.enqueue_next(self.scenes[scene].clips[track].as_ref()); } } @@ -315,7 +315,7 @@ impl App { /// Stop all playing clips pub(crate) fn stop_all (&mut self) { for track in 0..self.tracks.len() { - self.tracks[track].player.enqueue_next(None); + self.tracks[track].sequencer.enqueue_next(None); } } @@ -324,14 +324,14 @@ impl App { use Selection::*; match self.selected { Track(t) => { - self.tracks[t].player.enqueue_next(None) + self.tracks[t].sequencer.enqueue_next(None) }, TrackClip { track, scene } => { - self.tracks[track].player.enqueue_next(self.scenes[scene].clips[track].as_ref()) + self.tracks[track].sequencer.enqueue_next(self.scenes[scene].clips[track].as_ref()) }, Scene(s) => { for t in 0..self.tracks.len() { - self.tracks[t].player.enqueue_next(self.scenes[s].clips[t].as_ref()) + self.tracks[t].sequencer.enqueue_next(self.scenes[s].clips[t].as_ref()) } }, _ => {} @@ -417,7 +417,7 @@ impl App { fn device_add_sampler (&mut self) -> Usually<()> { let name = self.jack.with_client(|c|c.name().to_string()); - let midi = self.track().expect("no active track").player.midi_outs[0].name(); + let midi = self.track().expect("no active track").sequencer.midi_outs[0].name(); let sampler = if let Ok(sampler) = Sampler::new( &self.jack, &format!("{}/Sampler", &self.track().expect("no active track").name), diff --git a/crates/app/src/model/scene.rs b/crates/app/src/model/scene.rs index 353133ac..21d4f4ee 100644 --- a/crates/app/src/model/scene.rs +++ b/crates/app/src/model/scene.rs @@ -24,7 +24,7 @@ impl Scene { Some(c) => tracks .get(track_index) .map(|track|{ - if let Some((_, Some(clip))) = track.player().play_clip() { + if let Some((_, Some(clip))) = track.sequencer().play_clip() { *clip.read().unwrap() == *c.read().unwrap() } else { false diff --git a/crates/app/src/model/track.rs b/crates/app/src/model/track.rs index 6cf670b5..5e68b04d 100644 --- a/crates/app/src/model/track.rs +++ b/crates/app/src/model/track.rs @@ -7,8 +7,8 @@ use crate::*; pub width: usize, /// Identifying color of track pub color: ItemTheme, - /// MIDI player state - pub player: MidiPlayer, + /// MIDI sequencer state + pub sequencer: Sequencer, /// Device chain pub devices: Vec, /// Inputs of 1st device @@ -17,52 +17,60 @@ use crate::*; pub audio_outs: Vec, } -has_clock!(|self: Track|self.player.clock); +has_clock!(|self: Track|self.sequencer.clock); -has_player!(|self: Track|self.player); +has_sequencer!(|self: Track|self.sequencer); impl Track { - pub const MIN_WIDTH: usize = 9; - /// Create a new track with only the default [MidiPlayer]. - pub fn new () -> Self { - Self::default() + /// Create a new track with only the default [Sequencer]. + pub fn new ( + name: &impl AsRef, + color: Option, + jack: &Jack, + clock: Option<&Clock>, + midi_from: &[PortConnect], + midi_to: &[PortConnect], + ) -> Usually { + Ok(Self { + name: name.as_ref().into(), + color: color.unwrap_or_default(), + sequencer: Sequencer::new( + format!("{}/sequencer", name.as_ref()), + jack, + clock, + None, + midi_from, + midi_to + )?, + ..Default::default() + }) } - /// Create a new track connecting the [MidiPlayer] to a [Sampler]. + /// Create a new track connecting the [Sequencer] to a [Sampler]. pub fn new_with_sampler ( + name: &impl AsRef, + color: Option, jack: &Jack, + clock: Option<&Clock>, midi_from: &[PortConnect], + midi_to: &[PortConnect], audio_from: &[&[PortConnect];2], audio_to: &[&[PortConnect];2], ) -> Usually { - let mut track = Self::new_sequencer(); - let name = jack.with_client(|c|c.name().to_string()); - let midi = track.player.midi_outs[0].name(); - let port = PortConnect::exact(format!("{name}:{midi}")); - let sampler = Sampler::new(jack, &"sampler", &[port], audio_from, audio_to)?; - track.devices.push(Device::Sampler(sampler)); + let mut track = Self::new( + name, color, jack, clock, midi_from, midi_to + )?; + track.devices.push(Device::Sampler(Sampler::new( + jack, + &"sampler", + &[PortConnect::exact(format!("{}:{}", + jack.with_client(|c|c.name().to_string()), + track.sequencer.midi_outs[0].name() + ))], + audio_from, + audio_to + )?)); Ok(track) } - pub fn width_inc (&mut self) { - self.width += 1; - } - pub fn width_dec (&mut self) { - if self.width > Track::MIN_WIDTH { - self.width -= 1; - } - } - pub fn sequencer (&self, mut nth: usize) -> Option<&MidiPlayer> { - for device in self.devices.iter() { - match device { - Device::Sequencer(s) => if nth == 0 { - return Some(s); - } else { - nth -= 1; - }, - _ => {} - } - } - None - } pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { for device in self.devices.iter() { match device { @@ -91,6 +99,26 @@ impl Track { } } +pub trait HasWidth { + const MIN_WIDTH: usize; + /// Increment track width. + fn width_inc (&mut self); + /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. + fn width_dec (&mut self); +} + +impl HasWidth for Track { + const MIN_WIDTH: usize = 9; + fn width_inc (&mut self) { + self.width += 1; + } + fn width_dec (&mut self) { + if self.width > Track::MIN_WIDTH { + self.width -= 1; + } + } +} + pub trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync { fn midi_ins (&self) -> &Vec; fn midi_outs (&self) -> &Vec; @@ -117,14 +145,14 @@ pub trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync fn track_toggle_record (&mut self) { if let Some(t) = self.selected().track() { let tracks = self.tracks_mut(); - tracks[t-1].player.recording = !tracks[t-1].player.recording; + tracks[t-1].sequencer.recording = !tracks[t-1].sequencer.recording; } } /// Toggle track monitoring fn track_toggle_monitor (&mut self) { if let Some(t) = self.selected().track() { let tracks = self.tracks_mut(); - tracks[t-1].player.monitoring = !tracks[t-1].player.monitoring; + tracks[t-1].sequencer.monitoring = !tracks[t-1].sequencer.monitoring; } } } diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index 271c8251..e0bb9340 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -336,8 +336,8 @@ impl<'a> ArrangerView<'a> { self.width_mid, ||self.tracks_with_sizes_scrolled(), move|t, track|{ - let rec = track.player.recording; - let mon = track.player.monitoring; + let rec = track.sequencer.recording; + let mon = track.sequencer.monitoring; let rec = if rec { White } else { track.color.darkest.rgb }; let mon = if mon { White } else { track.color.darkest.rgb }; let bg = if self.track_selected == Some(t) { @@ -377,10 +377,10 @@ impl<'a> ArrangerView<'a> { let label = Align::ne("Next clip:"); Tryptich::top(2).left(self.width_side, label).middle(self.width_mid, per_track_top( self.width_mid, ||self.tracks_with_sizes_scrolled(), |t, track|{ - let queued = track.player.next_clip.is_some(); + let queued = track.sequencer.next_clip.is_some(); let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ ")); let queued_clip = Thunk::new(||{ - Tui::bg(Reset, if let Some((_, clip)) = track.player.next_clip.as_ref() { + Tui::bg(Reset, if let Some((_, clip)) = track.sequencer.next_clip.as_ref() { if let Some(clip) = clip { clip.read().unwrap().name.clone() } else { @@ -1224,7 +1224,7 @@ impl std::fmt::Debug for PianoHorizontal { } // Update sequencer playhead indicator //self.now().set(0.); - //if let Some((ref started_at, Some(ref playing))) = self.player.play_clip { + //if let Some((ref started_at, Some(ref playing))) = self.sequencer.play_clip { //let clip = clip.read().unwrap(); //if *playing.read().unwrap() == *clip { //let pulse = self.current().pulse.get(); diff --git a/crates/cli/tek.rs b/crates/cli/tek.rs index e6434a26..6d185221 100644 --- a/crates/cli/tek.rs +++ b/crates/cli/tek.rs @@ -136,11 +136,22 @@ impl Cli { }, tracks: match mode { LaunchMode::Sequencer => vec![ - Track::new() + Track::new( + &name, + None, + jack, + None, + midi_froms.as_slice(), + midi_tos.as_slice() + )? ], LaunchMode::Groovebox | LaunchMode::Sampler => vec![ Track::new_with_sampler( + &name, + None, jack, + None, + midi_froms.as_slice(), midi_froms.as_slice(), audio_froms, audio_tos, diff --git a/crates/device/src/device.rs b/crates/device/src/device.rs index 74d76488..60ecaf1a 100644 --- a/crates/device/src/device.rs +++ b/crates/device/src/device.rs @@ -2,7 +2,7 @@ use crate::*; #[derive(Debug)] pub enum Device { - #[cfg(feature = "sequencer")] Sequencer(MidiPlayer), + #[cfg(feature = "sequencer")] Sequencer(Sequencer), #[cfg(feature = "sampler")] Sampler(Sampler), #[cfg(feature = "lv2")] Lv2(Lv2), // TODO #[cfg(feature = "vst2")] Vst2, // TODO diff --git a/crates/device/src/sequencer.rs b/crates/device/src/sequencer.rs index ba8ab65e..6f8ca260 100644 --- a/crates/device/src/sequencer.rs +++ b/crates/device/src/sequencer.rs @@ -22,6 +22,6 @@ mod seq_view; pub use self::seq_view::*; } #[cfg(test)] #[test] fn test_midi_play () { - let player = MidiPlayer::default(); - println!("{player:?}"); + let sequencer = Sequencer::default(); + println!("{sequencer:?}"); } diff --git a/crates/device/src/sequencer/seq_model.rs b/crates/device/src/sequencer/seq_model.rs index 810b97b3..db65cd55 100644 --- a/crates/device/src/sequencer/seq_model.rs +++ b/crates/device/src/sequencer/seq_model.rs @@ -1,27 +1,27 @@ -//! MIDI player +//! MIDI sequencer use crate::*; use tek_engine::jack::*; -pub trait HasPlayer { - fn player (&self) -> &impl MidiPlayerApi; - fn player_mut (&mut self) -> &mut impl MidiPlayerApi; +pub trait HasSequencer { + fn sequencer (&self) -> &impl MidiPlayerApi; + fn sequencer_mut (&mut self) -> &mut impl MidiPlayerApi; } -#[macro_export] macro_rules! has_player { +#[macro_export] macro_rules! has_sequencer { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? { - fn player (&$self) -> &impl MidiPlayerApi { &$cb } - fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb } + impl $(<$($L),*$($T $(: $U)?),*>)? HasSequencer for $Struct $(<$($L),*$($T),*>)? { + fn sequencer (&$self) -> &impl MidiPlayerApi { &$cb } + fn sequencer_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb } } } } pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {} -impl MidiPlayerApi for MidiPlayer {} +impl MidiPlayerApi for Sequencer {} /// Contains state for playing a clip -pub struct MidiPlayer { +pub struct Sequencer { /// State of clock and playhead pub clock: Clock, /// Start time and clip being played @@ -48,7 +48,7 @@ pub struct MidiPlayer { pub note_buf: Vec, } -impl Default for MidiPlayer { +impl Default for Sequencer { fn default () -> Self { Self { play_clip: None, @@ -69,7 +69,7 @@ impl Default for MidiPlayer { } } -impl MidiPlayer { +impl Sequencer { pub fn new ( name: impl AsRef, jack: &Jack, @@ -97,9 +97,9 @@ impl MidiPlayer { } } -impl std::fmt::Debug for MidiPlayer { +impl std::fmt::Debug for Sequencer { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("MidiPlayer") + f.debug_struct("Sequencer") .field("clock", &self.clock) .field("play_clip", &self.play_clip) .field("next_clip", &self.next_clip) @@ -107,20 +107,20 @@ impl std::fmt::Debug for MidiPlayer { } } -has_clock!(|self: MidiPlayer|self.clock); +has_clock!(|self: Sequencer|self.clock); -impl HasMidiIns for MidiPlayer { +impl HasMidiIns for Sequencer { fn midi_ins (&self) -> &Vec { &self.midi_ins } fn midi_ins_mut (&mut self) -> &mut Vec { &mut self.midi_ins } } -impl HasMidiOuts for MidiPlayer { +impl HasMidiOuts for Sequencer { fn midi_outs (&self) -> &Vec { &self.midi_outs } fn midi_outs_mut (&mut self) -> &mut Vec { &mut self.midi_outs } fn midi_note (&mut self) -> &mut Vec { &mut self.note_buf } } -/// Hosts the JACK callback for a single MIDI player +/// Hosts the JACK callback for a single MIDI sequencer pub struct PlayerAudio<'a, T: MidiPlayerApi>( /// Player pub &'a mut T, @@ -130,7 +130,7 @@ pub struct PlayerAudio<'a, T: MidiPlayerApi>( pub &'a mut Vec>>, ); -/// JACK process callback for a sequencer's clip player/recorder. +/// JACK process callback for a sequencer's clip sequencer/recorder. impl Audio for PlayerAudio<'_, T> { fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { let model = &mut self.0; @@ -157,7 +157,7 @@ impl Audio for PlayerAudio<'_, T> { } } -impl MidiRecordApi for MidiPlayer { +impl MidiRecordApi for Sequencer { fn recording (&self) -> bool { self.recording } @@ -181,13 +181,13 @@ impl MidiRecordApi for MidiPlayer { } } -impl MidiPlaybackApi for MidiPlayer { +impl MidiPlaybackApi for Sequencer { fn notes_out (&self) -> &Arc> { &self.notes_out } } -impl HasPlayClip for MidiPlayer { +impl HasPlayClip for Sequencer { fn reset (&self) -> bool { self.reset } From 7f255eaea89b838dc99c0f2a398e669caa06576a Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 10 May 2025 20:45:18 +0300 Subject: [PATCH 04/15] refactor audio.rs --- crates/app/src/audio.rs | 129 +++++++++++----------- crates/app/src/model/track.rs | 2 +- crates/device/src/sampler/sampler_midi.rs | 24 ++++ crates/engine/src/midi.rs | 1 + 4 files changed, 88 insertions(+), 68 deletions(-) diff --git a/crates/app/src/audio.rs b/crates/app/src/audio.rs index 0081d093..39d23f13 100644 --- a/crates/app/src/audio.rs +++ b/crates/app/src/audio.rs @@ -1,76 +1,14 @@ use crate::*; -impl HasJack for App { fn jack (&self) -> &Jack { &self.jack } } + audio!( |self: App, client, scope|{ - - // Start profiling cycle let t0 = self.perf.get_t0(); - - // Update transport clock self.clock().update_from_scope(scope).unwrap(); - - // Collect MIDI input (TODO preallocate large buffers) - let midi_in = self.midi_ins.iter() - .map(|port|port.port().iter(scope) - .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) - .collect::>()) - .collect::>(); - - // Update standalone sampler - //if let Some(sampler) = self.sampler.as_mut() { - //if Control::Quit == SamplerAudio(sampler).process(client, scope) { - //return Control::Quit - //} - //for port in midi_in.iter() { - //for message in port.iter() { - //match message { - //Ok(M - //} - //} - //} - //} - - // TODO move these to editor and sampler?: - //for port in midi_in.iter() { - //for event in port.iter() { - //match event { - //(time, Ok(LiveEvent::Midi {message, ..})) => match message { - //MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => { - //editor.set_note_pos(key.as_int() as usize); - //}, - //MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = ( - //self.editor.as_ref(), - //self.sampler.as_ref(), - //) => { - //// TODO: give sampler its own cursor - //if let Some(sample) = &sampler.mapped[editor.note_pos()] { - //sample.write().unwrap().handle_cc(*controller, *value) - //} - //} - //_ =>{} - //}, - //_ =>{} - //} - //} - //} - - // Update track sequencers and devices - for track in self.tracks.iter_mut() { - if Control::Quit == PlayerAudio( - track.sequencer_mut(), &mut self.note_buf, &mut self.midi_buf - ).process(client, scope) { - return Control::Quit - } - for device in track.devices.iter_mut() { - if Control::Quit == DeviceAudio(device).process(client, scope) { - return Control::Quit - } - } - } - - // End profiling cycle + let midi_in = self.collect_midi_input(scope); + self.update_editor_cursor(&midi_in); + let result = self.render_tracks(client, scope); self.perf.update_from_jack_scope(t0, scope); - Control::Continue + result }; |self, event|{ use JackEvent::*; @@ -95,3 +33,60 @@ audio!( } } ); + +type CollectedMidiInput<'a> = Vec, MidiError>)>>; + +impl App { + + /// Collect MIDI input from app ports (TODO preallocate large buffers) + fn collect_midi_input <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { + self.midi_ins.iter() + .map(|port|port.port().iter(scope) + .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) + .collect::>()) + .collect::>() + } + + /// Update cursor in MIDI editor + fn update_editor_cursor (&self, midi_in: &CollectedMidiInput) { + if let Some(editor) = &self.editor { + let mut pitch: Option = None; + for port in midi_in.iter() { + for event in port.iter() { + if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {ref key, ..}, ..})) + = event + { + pitch = Some(key.clone()); + } + } + } + if let Some(pitch) = pitch { + editor.set_note_pos(pitch.as_int() as usize); + } + } + } + + /// Run audio callbacks for every track and every device + fn render_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { + for track in self.tracks.iter_mut() { + if Control::Quit == PlayerAudio( + track.sequencer_mut(), &mut self.note_buf, &mut self.midi_buf + ).process(client, scope) { + return Control::Quit + } + for device in track.devices.iter_mut() { + if Control::Quit == DeviceAudio(device).process(client, scope) { + return Control::Quit + } + } + } + Control::Continue + } + +} + +impl HasJack for App { + fn jack (&self) -> &Jack { + &self.jack + } +} diff --git a/crates/app/src/model/track.rs b/crates/app/src/model/track.rs index 5e68b04d..2d91244a 100644 --- a/crates/app/src/model/track.rs +++ b/crates/app/src/model/track.rs @@ -61,7 +61,7 @@ impl Track { )?; track.devices.push(Device::Sampler(Sampler::new( jack, - &"sampler", + &format!("{}/sampler", name.as_ref()), &[PortConnect::exact(format!("{}:{}", jack.with_client(|c|c.name().to_string()), track.sequencer.midi_outs[0].name() diff --git a/crates/device/src/sampler/sampler_midi.rs b/crates/device/src/sampler/sampler_midi.rs index 56885cc4..7a875e2e 100644 --- a/crates/device/src/sampler/sampler_midi.rs +++ b/crates/device/src/sampler/sampler_midi.rs @@ -52,3 +52,27 @@ impl Sample { } } } + +// TODO: +//for port in midi_in.iter() { + //for event in port.iter() { + //match event { + //(time, Ok(LiveEvent::Midi {message, ..})) => match message { + //MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => { + //editor.set_note_pos(key.as_int() as usize); + //}, + //MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = ( + //self.editor.as_ref(), + //self.sampler.as_ref(), + //) => { + //// TODO: give sampler its own cursor + //if let Some(sample) = &sampler.mapped[editor.note_pos()] { + //sample.write().unwrap().handle_cc(*controller, *value) + //} + //} + //_ =>{} + //}, + //_ =>{} + //} + //} +//} diff --git a/crates/engine/src/midi.rs b/crates/engine/src/midi.rs index b58425c8..216e253c 100644 --- a/crates/engine/src/midi.rs +++ b/crates/engine/src/midi.rs @@ -4,6 +4,7 @@ pub use ::midly::{ Smf, TrackEventKind, MidiMessage, + Error as MidiError, num::*, live::*, }; From 4127c141cc59f1310527fa5c3f1f1d6bbc31e3af Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 10 May 2025 21:21:12 +0300 Subject: [PATCH 05/15] editor: move to device crate --- crates/app/src/api.rs | 127 +------ crates/app/src/model.rs | 1 - crates/app/src/view.rs | 350 ------------------ crates/device/Cargo.toml | 5 +- crates/device/src/device.rs | 50 +-- crates/device/src/editor.rs | 7 + crates/device/src/editor/editor_api.rs | 126 +++++++ .../src/editor/editor_model.rs} | 10 +- crates/device/src/editor/editor_view.rs | 9 + crates/device/src/editor/editor_view_h.rs | 315 ++++++++++++++++ crates/device/src/editor/editor_view_v.rs | 37 ++ crates/device/src/lib.rs | 7 +- 12 files changed, 534 insertions(+), 510 deletions(-) create mode 100644 crates/device/src/editor.rs create mode 100644 crates/device/src/editor/editor_api.rs rename crates/{app/src/model/editor.rs => device/src/editor/editor_model.rs} (97%) create mode 100644 crates/device/src/editor/editor_view.rs create mode 100644 crates/device/src/editor/editor_view_h.rs create mode 100644 crates/device/src/editor/editor_view_v.rs diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index 4e4ae4b0..ce88fe99 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -58,7 +58,7 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Length(..))) } fn editor_pitch (&self) -> Option { - Some((self.editor().map(|e|e.note_pos()).unwrap() as u8).into()) + Some((self.editor().map(|e|e.get_note_pos()).unwrap() as u8).into()) } /// Width of display pub(crate) fn w (&self) -> u16 { @@ -202,78 +202,6 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm } } -#[tengri_proc::expose] impl MidiEditor { - fn _todo_opt_clip_stub (&self) -> Option>> { - todo!() - } - fn time_lock (&self) -> bool { - self.get_time_lock() - } - fn time_lock_toggled (&self) -> bool { - !self.get_time_lock() - } - - fn note_length (&self) -> usize { - self.get_note_len() - } - - fn note_pos (&self) -> usize { - self.get_note_pos() - } - fn note_pos_next (&self) -> usize { - self.get_note_pos() + 1 - } - fn note_pos_next_octave (&self) -> usize { - self.get_note_pos() + 12 - } - fn note_pos_prev (&self) -> usize { - self.get_note_pos().saturating_sub(1) - } - fn note_pos_prev_octave (&self) -> usize { - self.get_note_pos().saturating_sub(12) - } - - fn note_len (&self) -> usize { - self.get_note_len() - } - fn note_len_next (&self) -> usize { - self.get_note_len() + 1 - } - fn note_len_prev (&self) -> usize { - self.get_note_len().saturating_sub(1) - } - - fn note_range (&self) -> usize { - self.get_note_axis() - } - fn note_range_next (&self) -> usize { - self.get_note_axis() + 1 - } - fn note_range_prev (&self) -> usize { - self.get_note_axis().saturating_sub(1) - } - - fn time_pos (&self) -> usize { - self.get_time_pos() - } - fn time_pos_next (&self) -> usize { - self.get_time_pos() + self.time_zoom() - } - fn time_pos_prev (&self) -> usize { - self.get_time_pos().saturating_sub(self.time_zoom()) - } - - fn time_zoom (&self) -> usize { - self.get_time_zoom() - } - fn time_zoom_next (&self) -> usize { - self.get_time_zoom() + 1 - } - fn time_zoom_prev (&self) -> usize { - self.get_time_zoom().saturating_sub(1).max(1) - } -} - #[tengri_proc::command(App)] impl AppCommand { fn toggle_help (app: &mut App, value: bool) -> Perhaps { app.toggle_dialog(Some(Dialog::Help)); @@ -818,56 +746,3 @@ impl<'state> Context<'state, SamplerCommand> for App { todo!() } } - -#[tengri_proc::command(MidiEditor)] impl MidiEditCommand { - // TODO: 1-9 seek markers that by default start every 8th of the clip - fn note_append (editor: &mut MidiEditor) -> Perhaps { - editor.put_note(true); - Ok(None) - } - fn note_put (editor: &mut MidiEditor) -> Perhaps { - editor.put_note(false); - Ok(None) - } - fn note_del (editor: &mut MidiEditor) -> Perhaps { - todo!() - } - fn note_pos (editor: &mut MidiEditor, pos: usize) -> Perhaps { - editor.set_note_pos(pos.min(127)); - Ok(None) - } - fn note_len (editor: &mut MidiEditor, value: usize) -> Perhaps { - //let note_len = editor.get_note_len(); - //let time_zoom = editor.get_time_zoom(); - editor.set_note_len(value); - //if note_len / time_zoom != x / time_zoom { - editor.redraw(); - //} - Ok(None) - } - fn note_scroll (editor: &mut MidiEditor, value: usize) -> Perhaps { - editor.set_note_lo(value.min(127)); - Ok(None) - } - fn time_pos (editor: &mut MidiEditor, value: usize) -> Perhaps { - editor.set_time_pos(value); - Ok(None) - } - fn time_scroll (editor: &mut MidiEditor, value: usize) -> Perhaps { - editor.set_time_start(value); - Ok(None) - } - fn time_zoom (editor: &mut MidiEditor, value: usize) -> Perhaps { - editor.set_time_zoom(value); - editor.redraw(); - Ok(None) - } - fn time_lock (editor: &mut MidiEditor, value: bool) -> Perhaps { - editor.set_time_lock(value); - Ok(None) - } - fn show (editor: &mut MidiEditor, clip: Option>>) -> Perhaps { - editor.set_clip(clip.as_ref()); - Ok(None) - } -} diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index 41c4c875..46b52d58 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -1,7 +1,6 @@ use crate::*; mod dialog; pub use self::dialog::*; -mod editor; pub use self::editor::*; mod pool; pub use self::pool::*; mod selection; pub use self::selection::*; mod track; pub use self::track::*; diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index e0bb9340..9333d10a 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -927,353 +927,3 @@ content!(TuiOut: |self: ClipLength| { Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()), } }); - -/// A clip, rendered as a horizontal piano roll. -#[derive(Clone)] -pub struct PianoHorizontal { - pub clip: Option>>, - /// Buffer where the whole clip is rerendered on change - pub buffer: Arc>, - /// Size of actual notes area - pub size: Measure, - /// The display window - pub range: MidiRangeModel, - /// The note cursor - pub point: MidiPointModel, - /// The highlight color palette - pub color: ItemTheme, - /// Width of the keyboard - pub keys_width: u16, -} - -impl PianoHorizontal { - pub fn new (clip: Option<&Arc>>) -> Self { - let size = Measure::new(); - let mut range = MidiRangeModel::from((12, true)); - range.time_axis = size.x.clone(); - range.note_axis = size.y.clone(); - let piano = Self { - keys_width: 5, - size, - range, - buffer: RwLock::new(Default::default()).into(), - point: MidiPointModel::default(), - clip: clip.cloned(), - color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), - }; - piano.redraw(); - piano - } -} - -pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) - -> impl Iterator -{ - (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) -} - -content!(TuiOut:|self: PianoHorizontal| Tui::bg(Tui::g(40), Bsp::s( - Bsp::e( - Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())), - self.timeline() - ), - Bsp::e( - self.keys(), - self.size.of(Tui::bg(Tui::g(32), Bsp::b( - Fill::xy(self.notes()), - Fill::xy(self.cursor()), - ))) - ), -))); - -impl PianoHorizontal { - /// Draw the piano roll background. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize) { - for (y, note) in (0..=127).rev().enumerate() { - for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { - let cell = buf.get_mut(x, y).unwrap(); - cell.set_bg(clip.color.darkest.rgb); - if time % 384 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('│'); - } else if time % 96 == 0 { - cell.set_fg(clip.color.dark.rgb); - cell.set_char('╎'); - } else if time % note_len == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('┊'); - } else if (127 - note) % 12 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('='); - } else if (127 - note) % 6 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('—'); - } else { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('·'); - } - } - } - } - /// Draw the piano roll foreground. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { - let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); - let mut notes_on = [false;128]; - for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { - for (_y, note) in (0..=127).rev().enumerate() { - if let Some(cell) = buf.get_mut(x, note) { - if notes_on[note] { - cell.set_char('▂'); - cell.set_style(style); - } - } - } - let time_end = time_start + zoom; - for time in time_start..time_end.min(clip.length) { - for event in clip.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - if let Some(cell) = buf.get_mut(x, note) { - cell.set_char('█'); - cell.set_style(style); - } - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } - } - } - - } - } - fn notes (&self) -> impl Content { - let time_start = self.get_time_start(); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - let buffer = self.buffer.clone(); - ThunkRender::new(move|to: &mut TuiOut|{ - let source = buffer.read().unwrap(); - let [x0, y0, w, _h] = to.area().xywh(); - //if h as usize != note_axis { - //panic!("area height mismatch: {h} <> {note_axis}"); - //} - for (area_x, screen_x) in (x0..x0+w).enumerate() { - for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { - let source_x = time_start + area_x; - let source_y = note_hi - area_y; - // TODO: enable loop rollover: - //let source_x = (time_start + area_x) % source.width.max(1); - //let source_y = (note_hi - area_y) % source.height.max(1); - let is_in_x = source_x < source.width; - let is_in_y = source_y < source.height; - if is_in_x && is_in_y { - if let Some(source_cell) = source.get(source_x, source_y) { - if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { - *cell = source_cell.clone(); - } - } - } - } - } - }) - } - fn cursor (&self) -> impl Content { - let note_hi = self.get_note_hi(); - let note_lo = self.get_note_lo(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let time_pos = self.get_time_pos(); - let time_start = self.get_time_start(); - let time_zoom = self.get_time_zoom(); - let style = Some(Style::default().fg(self.color.lightest.rgb)); - ThunkRender::new(move|to: &mut TuiOut|{ - let [x0, y0, w, _] = to.area().xywh(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - if note == note_pos { - for x in 0..w { - let screen_x = x0 + x; - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_pos && time_pos < time_2 { - to.blit(&"█", screen_x, screen_y, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (screen_x + 1)..(screen_x + tail) { - to.blit(&"▂", x_tail, screen_y, style); - } - break - } - } - break - } - } - }) - } - fn keys (&self) -> impl Content { - let state = self; - let color = state.color; - let note_lo = state.get_note_lo(); - let note_hi = state.get_note_hi(); - let note_pos = state.get_note_pos(); - let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); - let off_style = Some(Style::default().fg(Tui::g(255))); - let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); - Fill::y(Fixed::x(self.keys_width, ThunkRender::new(move|to: &mut TuiOut|{ - let [x, y0, _w, _h] = to.area().xywh(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - to.blit(&to_key(note), x, screen_y, key_style); - if note > 127 { - continue - } - if note == note_pos { - to.blit(&format!("{:<5}", Note::pitch_to_name(note)), x, screen_y, on_style) - } else { - to.blit(&Note::pitch_to_name(note), x, screen_y, off_style) - }; - } - }))) - } - fn timeline (&self) -> impl Content + '_ { - Fill::x(Fixed::y(1, ThunkRender::new(move|to: &mut TuiOut|{ - let [x, y, w, _h] = to.area(); - let style = Some(Style::default().dim()); - let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { - let t = area_x as usize * self.time_zoom().get(); - if t < length { - to.blit(&"|", screen_x, y, style); - } - } - }))) - } -} - -has_size!(|self:PianoHorizontal|&self.size); - -impl TimeRange for PianoHorizontal { - fn time_len (&self) -> &AtomicUsize { self.range.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.range.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } -} - -impl NoteRange for PianoHorizontal { - fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } -} - -impl NotePoint for PianoHorizontal { - fn note_len (&self) -> &AtomicUsize { self.point.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } -} - -impl TimePoint for PianoHorizontal { - fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } -} - -impl MidiViewer for PianoHorizontal { - fn clip (&self) -> &Option>> { - &self.clip - } - fn clip_mut (&mut self) -> &mut Option>> { - &mut self.clip - } - /// Determine the required space to render the clip. - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { - (clip.length / self.range.time_zoom().get(), 128) - } - fn redraw (&self) { - *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { - let clip = clip.read().unwrap(); - let buf_size = self.buffer_size(&clip); - let mut buffer = BigBuffer::from(buf_size); - let note_len = self.get_note_len(); - let time_zoom = self.get_time_zoom(); - self.time_len().set(clip.length); - PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom, note_len); - PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); - buffer - } else { - Default::default() - } - } - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.color = clip.map(|p|p.read().unwrap().color) - .unwrap_or(ItemTheme::G[64]); - self.redraw(); - } -} - -impl std::fmt::Debug for PianoHorizontal { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - let buffer = self.buffer.read().unwrap(); - f.debug_struct("PianoHorizontal") - .field("time_zoom", &self.range.time_zoom) - .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) - .finish() - } -} - // Update sequencer playhead indicator - //self.now().set(0.); - //if let Some((ref started_at, Some(ref playing))) = self.sequencer.play_clip { - //let clip = clip.read().unwrap(); - //if *playing.read().unwrap() == *clip { - //let pulse = self.current().pulse.get(); - //let start = started_at.pulse.get(); - //let now = (pulse - start) % clip.length as f64; - //self.now().set(now); - //} - //} - -fn to_key (note: usize) -> &'static str { - match note % 12 { - 11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌", - 10 | 8 | 6 | 3 | 1 => " ", - _ => unreachable!(), - } -} - -pub struct OctaveVertical { - on: [bool; 12], - colors: [Color; 3] -} - -impl Default for OctaveVertical { - fn default () -> Self { - Self { - on: [false; 12], - colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] - } - } -} - -impl OctaveVertical { - fn color (&self, pitch: usize) -> Color { - let pitch = pitch % 12; - self.colors[if self.on[pitch] { 2 } else { - match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } - }] - } -} - -impl Content for OctaveVertical { - fn content (&self) -> impl Render { - row!( - Tui::fg_bg(self.color(0), self.color(1), "▙"), - Tui::fg_bg(self.color(2), self.color(3), "▙"), - Tui::fg_bg(self.color(4), self.color(5), "▌"), - Tui::fg_bg(self.color(6), self.color(7), "▟"), - Tui::fg_bg(self.color(8), self.color(9), "▟"), - Tui::fg_bg(self.color(10), self.color(11), "▟"), - ) - } -} diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml index f07255a4..aacae057 100644 --- a/crates/device/Cargo.toml +++ b/crates/device/Cargo.toml @@ -16,10 +16,11 @@ wavers = { workspace = true, optional = true } winit = { workspace = true, optional = true } [features] -default = [ "clock", "sequencer", "sampler", "lv2" ] +default = [ "clock", "editor", "sequencer", "sampler", "lv2" ] clock = [] -sampler = [ "symphonia", "wavers" ] +editor = [] sequencer = [ "clock", "uuid" ] +sampler = [ "symphonia", "wavers" ] lv2 = [ "livi", "winit" ] vst2 = [] vst3 = [] diff --git a/crates/device/src/device.rs b/crates/device/src/device.rs index 60ecaf1a..ee6a404f 100644 --- a/crates/device/src/device.rs +++ b/crates/device/src/device.rs @@ -2,13 +2,18 @@ use crate::*; #[derive(Debug)] pub enum Device { - #[cfg(feature = "sequencer")] Sequencer(Sequencer), - #[cfg(feature = "sampler")] Sampler(Sampler), - #[cfg(feature = "lv2")] Lv2(Lv2), // TODO - #[cfg(feature = "vst2")] Vst2, // TODO - #[cfg(feature = "vst3")] Vst3, // TODO - #[cfg(feature = "clap")] Clap, // TODO - #[cfg(feature = "sf2")] Sf2, // TODO + #[cfg(feature = "sampler")] + Sampler(Sampler), + #[cfg(feature = "lv2")] // TODO + Lv2(Lv2), + #[cfg(feature = "vst2")] // TODO + Vst2, + #[cfg(feature = "vst3")] // TODO + Vst3, + #[cfg(feature = "clap")] // TODO + Clap, + #[cfg(feature = "sf2")] // TODO + Sf2, } impl Device { @@ -25,19 +30,22 @@ pub struct DeviceAudio<'a>(pub &'a mut Device); audio!(|self: DeviceAudio<'a>, client, scope|{ use Device::*; match self.0 { - #[cfg(feature = "sequencer")] Sequencer(sequencer) => - { Control::Continue /* TODO */ }, - #[cfg(feature = "sampler")] Sampler(sampler) => - SamplerAudio(sampler).process(client, scope), - #[cfg(feature = "lv2")] Lv2(lv2) => - { todo!() }, // TODO - #[cfg(feature = "vst2")] Vst2 => - { todo!() }, // TODO - #[cfg(feature = "vst3")] Vst3 => - { todo!() }, // TODO - #[cfg(feature = "clap")] Clap => - { todo!() }, // TODO - #[cfg(feature = "sf2")] Sf2 => - { todo!() }, // TODO + #[cfg(feature = "sampler")] + Sampler(sampler) => SamplerAudio(sampler).process(client, scope), + + #[cfg(feature = "lv2")] + Lv2(lv2) => lv2.process(client, scope), + + #[cfg(feature = "vst2")] + Vst2 => { todo!() }, // TODO + + #[cfg(feature = "vst3")] + Vst3 => { todo!() }, // TODO + + #[cfg(feature = "clap")] + Clap => { todo!() }, // TODO + + #[cfg(feature = "sf2")] + Sf2 => { todo!() }, // TODO } }); diff --git a/crates/device/src/editor.rs b/crates/device/src/editor.rs new file mode 100644 index 00000000..eb81e5b9 --- /dev/null +++ b/crates/device/src/editor.rs @@ -0,0 +1,7 @@ +use crate::*; + +mod editor_api; pub use self::editor_api::*; +mod editor_model; pub use self::editor_model::*; +mod editor_view; pub use self::editor_view::*; +mod editor_view_h; pub use self::editor_view_h::*; +mod editor_view_v; pub use self::editor_view_v::*; diff --git a/crates/device/src/editor/editor_api.rs b/crates/device/src/editor/editor_api.rs new file mode 100644 index 00000000..d2cee7db --- /dev/null +++ b/crates/device/src/editor/editor_api.rs @@ -0,0 +1,126 @@ +use crate::*; + +#[tengri_proc::expose] impl MidiEditor { + fn _todo_opt_clip_stub (&self) -> Option>> { + todo!() + } + fn time_lock (&self) -> bool { + self.get_time_lock() + } + fn time_lock_toggled (&self) -> bool { + !self.get_time_lock() + } + + fn note_length (&self) -> usize { + self.get_note_len() + } + + fn note_pos (&self) -> usize { + self.get_note_pos() + } + fn note_pos_next (&self) -> usize { + self.get_note_pos() + 1 + } + fn note_pos_next_octave (&self) -> usize { + self.get_note_pos() + 12 + } + fn note_pos_prev (&self) -> usize { + self.get_note_pos().saturating_sub(1) + } + fn note_pos_prev_octave (&self) -> usize { + self.get_note_pos().saturating_sub(12) + } + + fn note_len (&self) -> usize { + self.get_note_len() + } + fn note_len_next (&self) -> usize { + self.get_note_len() + 1 + } + fn note_len_prev (&self) -> usize { + self.get_note_len().saturating_sub(1) + } + + fn note_range (&self) -> usize { + self.get_note_axis() + } + fn note_range_next (&self) -> usize { + self.get_note_axis() + 1 + } + fn note_range_prev (&self) -> usize { + self.get_note_axis().saturating_sub(1) + } + + fn time_pos (&self) -> usize { + self.get_time_pos() + } + fn time_pos_next (&self) -> usize { + self.get_time_pos() + self.time_zoom() + } + fn time_pos_prev (&self) -> usize { + self.get_time_pos().saturating_sub(self.time_zoom()) + } + + fn time_zoom (&self) -> usize { + self.get_time_zoom() + } + fn time_zoom_next (&self) -> usize { + self.get_time_zoom() + 1 + } + fn time_zoom_prev (&self) -> usize { + self.get_time_zoom().saturating_sub(1).max(1) + } +} + +#[tengri_proc::command(MidiEditor)] impl MidiEditCommand { + // TODO: 1-9 seek markers that by default start every 8th of the clip + fn note_append (editor: &mut MidiEditor) -> Perhaps { + editor.put_note(true); + Ok(None) + } + fn note_put (editor: &mut MidiEditor) -> Perhaps { + editor.put_note(false); + Ok(None) + } + fn note_del (editor: &mut MidiEditor) -> Perhaps { + todo!() + } + fn note_pos (editor: &mut MidiEditor, pos: usize) -> Perhaps { + editor.set_note_pos(pos.min(127)); + Ok(None) + } + fn note_len (editor: &mut MidiEditor, value: usize) -> Perhaps { + //let note_len = editor.get_note_len(); + //let time_zoom = editor.get_time_zoom(); + editor.set_note_len(value); + //if note_len / time_zoom != x / time_zoom { + editor.redraw(); + //} + Ok(None) + } + fn note_scroll (editor: &mut MidiEditor, value: usize) -> Perhaps { + editor.set_note_lo(value.min(127)); + Ok(None) + } + fn time_pos (editor: &mut MidiEditor, value: usize) -> Perhaps { + editor.set_time_pos(value); + Ok(None) + } + fn time_scroll (editor: &mut MidiEditor, value: usize) -> Perhaps { + editor.set_time_start(value); + Ok(None) + } + fn time_zoom (editor: &mut MidiEditor, value: usize) -> Perhaps { + editor.set_time_zoom(value); + editor.redraw(); + Ok(None) + } + fn time_lock (editor: &mut MidiEditor, value: bool) -> Perhaps { + editor.set_time_lock(value); + Ok(None) + } + fn show (editor: &mut MidiEditor, clip: Option>>) -> Perhaps { + editor.set_clip(clip.as_ref()); + Ok(None) + } +} diff --git a/crates/app/src/model/editor.rs b/crates/device/src/editor/editor_model.rs similarity index 97% rename from crates/app/src/model/editor.rs rename to crates/device/src/editor/editor_model.rs index fec878b1..5c648b70 100644 --- a/crates/app/src/model/editor.rs +++ b/crates/device/src/editor/editor_model.rs @@ -25,15 +25,6 @@ impl Default for MidiEditor { } } - -has_size!(|self: MidiEditor|&self.size); - -content!(TuiOut: |self: MidiEditor| { - self.autoscroll(); - //self.autozoom(); - self.size.of(&self.mode) -}); - from!(|clip: &Arc>|MidiEditor = { let model = Self::from(Some(clip.clone())); model.redraw(); @@ -166,3 +157,4 @@ pub trait HasEditor { } }; } + diff --git a/crates/device/src/editor/editor_view.rs b/crates/device/src/editor/editor_view.rs new file mode 100644 index 00000000..9a7e4f48 --- /dev/null +++ b/crates/device/src/editor/editor_view.rs @@ -0,0 +1,9 @@ +use crate::*; + +has_size!(|self: MidiEditor|&self.size); + +content!(TuiOut: |self: MidiEditor| { + self.autoscroll(); + //self.autozoom(); + self.size.of(&self.mode) +}); diff --git a/crates/device/src/editor/editor_view_h.rs b/crates/device/src/editor/editor_view_h.rs new file mode 100644 index 00000000..72365a09 --- /dev/null +++ b/crates/device/src/editor/editor_view_h.rs @@ -0,0 +1,315 @@ +use crate::*; + +/// A clip, rendered as a horizontal piano roll. +#[derive(Clone)] +pub struct PianoHorizontal { + pub clip: Option>>, + /// Buffer where the whole clip is rerendered on change + pub buffer: Arc>, + /// Size of actual notes area + pub size: Measure, + /// The display window + pub range: MidiRangeModel, + /// The note cursor + pub point: MidiPointModel, + /// The highlight color palette + pub color: ItemTheme, + /// Width of the keyboard + pub keys_width: u16, +} + +impl PianoHorizontal { + pub fn new (clip: Option<&Arc>>) -> Self { + let size = Measure::new(); + let mut range = MidiRangeModel::from((12, true)); + range.time_axis = size.x.clone(); + range.note_axis = size.y.clone(); + let piano = Self { + keys_width: 5, + size, + range, + buffer: RwLock::new(Default::default()).into(), + point: MidiPointModel::default(), + clip: clip.cloned(), + color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), + }; + piano.redraw(); + piano + } +} + +pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) + -> impl Iterator +{ + (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) +} + +content!(TuiOut:|self: PianoHorizontal| Tui::bg(Tui::g(40), Bsp::s( + Bsp::e( + Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())), + self.timeline() + ), + Bsp::e( + self.keys(), + self.size.of(Tui::bg(Tui::g(32), Bsp::b( + Fill::xy(self.notes()), + Fill::xy(self.cursor()), + ))) + ), +))); + +impl PianoHorizontal { + /// Draw the piano roll background. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize) { + for (y, note) in (0..=127).rev().enumerate() { + for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { + let cell = buf.get_mut(x, y).unwrap(); + cell.set_bg(clip.color.darkest.rgb); + if time % 384 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('│'); + } else if time % 96 == 0 { + cell.set_fg(clip.color.dark.rgb); + cell.set_char('╎'); + } else if time % note_len == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('┊'); + } else if (127 - note) % 12 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('='); + } else if (127 - note) % 6 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('—'); + } else { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('·'); + } + } + } + } + /// Draw the piano roll foreground. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { + let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); + let mut notes_on = [false;128]; + for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { + for (_y, note) in (0..=127).rev().enumerate() { + if let Some(cell) = buf.get_mut(x, note) { + if notes_on[note] { + cell.set_char('▂'); + cell.set_style(style); + } + } + } + let time_end = time_start + zoom; + for time in time_start..time_end.min(clip.length) { + for event in clip.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + if let Some(cell) = buf.get_mut(x, note) { + cell.set_char('█'); + cell.set_style(style); + } + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} + } + } + } + + } + } + fn notes (&self) -> impl Content { + let time_start = self.get_time_start(); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + let buffer = self.buffer.clone(); + ThunkRender::new(move|to: &mut TuiOut|{ + let source = buffer.read().unwrap(); + let [x0, y0, w, _h] = to.area().xywh(); + //if h as usize != note_axis { + //panic!("area height mismatch: {h} <> {note_axis}"); + //} + for (area_x, screen_x) in (x0..x0+w).enumerate() { + for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { + let source_x = time_start + area_x; + let source_y = note_hi - area_y; + // TODO: enable loop rollover: + //let source_x = (time_start + area_x) % source.width.max(1); + //let source_y = (note_hi - area_y) % source.height.max(1); + let is_in_x = source_x < source.width; + let is_in_y = source_y < source.height; + if is_in_x && is_in_y { + if let Some(source_cell) = source.get(source_x, source_y) { + if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { + *cell = source_cell.clone(); + } + } + } + } + } + }) + } + fn cursor (&self) -> impl Content { + let note_hi = self.get_note_hi(); + let note_lo = self.get_note_lo(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let time_pos = self.get_time_pos(); + let time_start = self.get_time_start(); + let time_zoom = self.get_time_zoom(); + let style = Some(Style::default().fg(self.color.lightest.rgb)); + ThunkRender::new(move|to: &mut TuiOut|{ + let [x0, y0, w, _] = to.area().xywh(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + if note == note_pos { + for x in 0..w { + let screen_x = x0 + x; + let time_1 = time_start + x as usize * time_zoom; + let time_2 = time_1 + time_zoom; + if time_1 <= time_pos && time_pos < time_2 { + to.blit(&"█", screen_x, screen_y, style); + let tail = note_len as u16 / time_zoom as u16; + for x_tail in (screen_x + 1)..(screen_x + tail) { + to.blit(&"▂", x_tail, screen_y, style); + } + break + } + } + break + } + } + }) + } + fn keys (&self) -> impl Content { + let state = self; + let color = state.color; + let note_lo = state.get_note_lo(); + let note_hi = state.get_note_hi(); + let note_pos = state.get_note_pos(); + let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); + let off_style = Some(Style::default().fg(Tui::g(255))); + let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); + Fill::y(Fixed::x(self.keys_width, ThunkRender::new(move|to: &mut TuiOut|{ + let [x, y0, _w, _h] = to.area().xywh(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + to.blit(&to_key(note), x, screen_y, key_style); + if note > 127 { + continue + } + if note == note_pos { + to.blit(&format!("{:<5}", Note::pitch_to_name(note)), x, screen_y, on_style) + } else { + to.blit(&Note::pitch_to_name(note), x, screen_y, off_style) + }; + } + }))) + } + fn timeline (&self) -> impl Content + '_ { + Fill::x(Fixed::y(1, ThunkRender::new(move|to: &mut TuiOut|{ + let [x, y, w, _h] = to.area(); + let style = Some(Style::default().dim()); + let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { + let t = area_x as usize * self.time_zoom().get(); + if t < length { + to.blit(&"|", screen_x, y, style); + } + } + }))) + } +} + +has_size!(|self:PianoHorizontal|&self.size); + +impl TimeRange for PianoHorizontal { + fn time_len (&self) -> &AtomicUsize { self.range.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.range.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } +} + +impl NoteRange for PianoHorizontal { + fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } +} + +impl NotePoint for PianoHorizontal { + fn note_len (&self) -> &AtomicUsize { self.point.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } +} + +impl TimePoint for PianoHorizontal { + fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } +} + +impl MidiViewer for PianoHorizontal { + fn clip (&self) -> &Option>> { + &self.clip + } + fn clip_mut (&mut self) -> &mut Option>> { + &mut self.clip + } + /// Determine the required space to render the clip. + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { + (clip.length / self.range.time_zoom().get(), 128) + } + fn redraw (&self) { + *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { + let clip = clip.read().unwrap(); + let buf_size = self.buffer_size(&clip); + let mut buffer = BigBuffer::from(buf_size); + let note_len = self.get_note_len(); + let time_zoom = self.get_time_zoom(); + self.time_len().set(clip.length); + PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom, note_len); + PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); + buffer + } else { + Default::default() + } + } + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.color = clip.map(|p|p.read().unwrap().color) + .unwrap_or(ItemTheme::G[64]); + self.redraw(); + } +} + +impl std::fmt::Debug for PianoHorizontal { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + let buffer = self.buffer.read().unwrap(); + f.debug_struct("PianoHorizontal") + .field("time_zoom", &self.range.time_zoom) + .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) + .finish() + } +} + // Update sequencer playhead indicator + //self.now().set(0.); + //if let Some((ref started_at, Some(ref playing))) = self.sequencer.play_clip { + //let clip = clip.read().unwrap(); + //if *playing.read().unwrap() == *clip { + //let pulse = self.current().pulse.get(); + //let start = started_at.pulse.get(); + //let now = (pulse - start) % clip.length as f64; + //self.now().set(now); + //} + //} + +fn to_key (note: usize) -> &'static str { + match note % 12 { + 11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌", + 10 | 8 | 6 | 3 | 1 => " ", + _ => unreachable!(), + } +} diff --git a/crates/device/src/editor/editor_view_v.rs b/crates/device/src/editor/editor_view_v.rs new file mode 100644 index 00000000..493c6de9 --- /dev/null +++ b/crates/device/src/editor/editor_view_v.rs @@ -0,0 +1,37 @@ +use crate::*; + +pub struct OctaveVertical { + on: [bool; 12], + colors: [Color; 3] +} + +impl Default for OctaveVertical { + fn default () -> Self { + Self { + on: [false; 12], + colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] + } + } +} + +impl OctaveVertical { + fn color (&self, pitch: usize) -> Color { + let pitch = pitch % 12; + self.colors[if self.on[pitch] { 2 } else { + match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } + }] + } +} + +impl Content for OctaveVertical { + fn content (&self) -> impl Render { + row!( + Tui::fg_bg(self.color(0), self.color(1), "▙"), + Tui::fg_bg(self.color(2), self.color(3), "▙"), + Tui::fg_bg(self.color(4), self.color(5), "▌"), + Tui::fg_bg(self.color(6), self.color(7), "▟"), + Tui::fg_bg(self.color(8), self.color(9), "▟"), + Tui::fg_bg(self.color(10), self.color(11), "▟"), + ) + } +} diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 16f34bf2..88c4cd6f 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -3,7 +3,8 @@ pub(crate) use std::cmp::Ord; pub(crate) use std::fmt::{Debug, Formatter}; pub(crate) use std::thread::JoinHandle; -pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}}; +pub(crate) use std::sync::{Arc, RwLock}; +pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}; pub(crate) use std::fs::File; pub(crate) use std::path::PathBuf; pub(crate) use std::error::Error; @@ -14,6 +15,7 @@ pub(crate) use ::tek_engine::*; pub(crate) use ::tek_engine::midi::{u7, LiveEvent, MidiMessage}; pub(crate) use ::tek_engine::jack::{Control, ProcessScope, MidiWriter, RawMidi}; pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}}; +pub(crate) use Color::*; mod device; pub use self::device::*; @@ -21,6 +23,9 @@ pub use self::device::*; #[cfg(feature = "clock")] mod clock; #[cfg(feature = "clock")] pub use self::clock::*; +#[cfg(feature = "editor")] mod editor; +#[cfg(feature = "editor")] pub use self::editor::*; + #[cfg(feature = "sequencer")] mod sequencer; #[cfg(feature = "sequencer")] pub use self::sequencer::*; From 2ef9628ab8e9a13a97fcd047c501745e73d270a5 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 10 May 2025 21:44:27 +0300 Subject: [PATCH 06/15] device: add RMSMeter --- crates/device/Cargo.toml | 3 ++- crates/device/src/meter.rs | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 crates/device/src/meter.rs diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml index aacae057..0f8b2230 100644 --- a/crates/device/Cargo.toml +++ b/crates/device/Cargo.toml @@ -19,8 +19,9 @@ winit = { workspace = true, optional = true } default = [ "clock", "editor", "sequencer", "sampler", "lv2" ] clock = [] editor = [] +meter = [] sequencer = [ "clock", "uuid" ] -sampler = [ "symphonia", "wavers" ] +sampler = [ "meter", "symphonia", "wavers" ] lv2 = [ "livi", "winit" ] vst2 = [] vst3 = [] diff --git a/crates/device/src/meter.rs b/crates/device/src/meter.rs new file mode 100644 index 00000000..74dba443 --- /dev/null +++ b/crates/device/src/meter.rs @@ -0,0 +1,16 @@ +use crate::*; + +#[derive(Debug, Default, Clone)] +pub struct RMSMeter(f32); + +impl RMSMeter { + pub fn set (&mut self, samples: &[u32]) { + let sum: usize = samples.iter().map(|s|*s as usize).reduce(|sum, sample|sum + sample) + .unwrap_or(0); + self.0 = (sum as f32 / samples.len() as f32).sqrt(); + } +} + +render!(TuiOut: |self: RMSMeter, to| { + let [x, y, w, h] = to.area(); +}); From e5752ea4b0089f0cef5a8e7044c468c580561fa5 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 10 May 2025 21:44:36 +0300 Subject: [PATCH 07/15] fix warnings --- crates/device/src/clock/clock_model.rs | 2 +- crates/device/src/editor.rs | 4 +--- crates/device/src/editor/editor_api.rs | 2 +- crates/device/src/editor/editor_model.rs | 2 +- crates/device/src/editor/editor_view_h.rs | 2 +- crates/device/src/lib.rs | 3 +++ crates/device/src/lv2.rs | 8 ++++---- crates/device/src/lv2/lv2_model.rs | 1 - crates/device/src/sampler.rs | 2 -- crates/device/src/sampler/sampler_api.rs | 3 --- crates/device/src/sampler/sampler_view.rs | 2 +- crates/device/src/sequencer.rs | 4 ++-- crates/device/src/sequencer/seq_model.rs | 3 +-- 13 files changed, 16 insertions(+), 22 deletions(-) diff --git a/crates/device/src/clock/clock_model.rs b/crates/device/src/clock/clock_model.rs index f16e3558..e9016264 100644 --- a/crates/device/src/clock/clock_model.rs +++ b/crates/device/src/clock/clock_model.rs @@ -29,7 +29,7 @@ pub struct Clock { } impl std::fmt::Debug for Clock { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Clock") .field("timebase", &self.timebase) .field("chunk", &self.chunk) diff --git a/crates/device/src/editor.rs b/crates/device/src/editor.rs index eb81e5b9..5ce20dd0 100644 --- a/crates/device/src/editor.rs +++ b/crates/device/src/editor.rs @@ -1,7 +1,5 @@ -use crate::*; - mod editor_api; pub use self::editor_api::*; mod editor_model; pub use self::editor_model::*; -mod editor_view; pub use self::editor_view::*; +mod editor_view; //pub use self::editor_view::*; mod editor_view_h; pub use self::editor_view_h::*; mod editor_view_v; pub use self::editor_view_v::*; diff --git a/crates/device/src/editor/editor_api.rs b/crates/device/src/editor/editor_api.rs index d2cee7db..a1c6a6e5 100644 --- a/crates/device/src/editor/editor_api.rs +++ b/crates/device/src/editor/editor_api.rs @@ -82,7 +82,7 @@ use crate::*; editor.put_note(false); Ok(None) } - fn note_del (editor: &mut MidiEditor) -> Perhaps { + fn note_del (_editor: &mut MidiEditor) -> Perhaps { todo!() } fn note_pos (editor: &mut MidiEditor, pos: usize) -> Perhaps { diff --git a/crates/device/src/editor/editor_model.rs b/crates/device/src/editor/editor_model.rs index 5c648b70..a92e96ba 100644 --- a/crates/device/src/editor/editor_model.rs +++ b/crates/device/src/editor/editor_model.rs @@ -9,7 +9,7 @@ pub struct MidiEditor { } impl std::fmt::Debug for MidiEditor { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("MidiEditor") .field("mode", &self.mode) .finish() diff --git a/crates/device/src/editor/editor_view_h.rs b/crates/device/src/editor/editor_view_h.rs index 72365a09..b9bf650b 100644 --- a/crates/device/src/editor/editor_view_h.rs +++ b/crates/device/src/editor/editor_view_h.rs @@ -286,7 +286,7 @@ impl MidiViewer for PianoHorizontal { } impl std::fmt::Debug for PianoHorizontal { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { let buffer = self.buffer.read().unwrap(); f.debug_struct("PianoHorizontal") .field("time_zoom", &self.range.time_zoom) diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 88c4cd6f..f15f3a7a 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -32,6 +32,9 @@ pub use self::device::*; #[cfg(feature = "sampler")] mod sampler; #[cfg(feature = "sampler")] pub use self::sampler::*; +#[cfg(feature = "meter")] mod meter; +#[cfg(feature = "meter")] pub use self::meter::*; + #[cfg(feature = "lv2")] mod lv2; #[cfg(feature = "lv2")] pub use self::lv2::*; diff --git a/crates/device/src/lv2.rs b/crates/device/src/lv2.rs index b1f0b505..4610b30b 100644 --- a/crates/device/src/lv2.rs +++ b/crates/device/src/lv2.rs @@ -1,5 +1,5 @@ mod lv2_model; pub use self::lv2_model::*; -mod lv2_audio; pub use self::lv2_audio::*; -mod lv2_gui; pub use self::lv2_gui::*; -mod lv2_tui; pub use self::lv2_tui::*; -pub(self) use std::thread::JoinHandle; +mod lv2_audio; //pub use self::lv2_audio::*; +mod lv2_gui; pub use self::lv2_gui::*; +mod lv2_tui; //pub use self::lv2_tui::*; +//pub(self) use std::thread::JoinHandle; diff --git a/crates/device/src/lv2/lv2_model.rs b/crates/device/src/lv2/lv2_model.rs index 875a31bb..64679f75 100644 --- a/crates/device/src/lv2/lv2_model.rs +++ b/crates/device/src/lv2/lv2_model.rs @@ -1,5 +1,4 @@ use crate::*; -use super::*; /// A LV2 plugin. #[derive(Debug)] diff --git a/crates/device/src/sampler.rs b/crates/device/src/sampler.rs index 6423684b..b201fb54 100644 --- a/crates/device/src/sampler.rs +++ b/crates/device/src/sampler.rs @@ -1,5 +1,3 @@ -use crate::*; - pub(crate) use symphonia::{ core::{ formats::Packet, diff --git a/crates/device/src/sampler/sampler_api.rs b/crates/device/src/sampler/sampler_api.rs index 5d1279da..e374f88e 100644 --- a/crates/device/src/sampler/sampler_api.rs +++ b/crates/device/src/sampler/sampler_api.rs @@ -1,8 +1,5 @@ use crate::*; -macro_rules! cmd { ($cmd:expr) => {{ $cmd; None }}; } -macro_rules! cmd_todo { ($msg:literal) => {{ println!($msg); None }}; } - #[tengri_proc::expose] impl Sampler { //fn file_browser_filter (&self) -> Arc { diff --git a/crates/device/src/sampler/sampler_view.rs b/crates/device/src/sampler/sampler_view.rs index 2b5c3fad..d984b3a4 100644 --- a/crates/device/src/sampler/sampler_view.rs +++ b/crates/device/src/sampler/sampler_view.rs @@ -62,7 +62,7 @@ impl Sampler { Fixed::x(12, Map::south( 1, move||(note_lo..=note_hi).rev(), - move|note, i| { + move|note, _index| { //let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a)))); let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset }; let mut fg = Tui::g(160); diff --git a/crates/device/src/sequencer.rs b/crates/device/src/sequencer.rs index 6f8ca260..df991b7f 100644 --- a/crates/device/src/sequencer.rs +++ b/crates/device/src/sequencer.rs @@ -1,11 +1,10 @@ -use crate::*; - mod seq_clip; pub use self::seq_clip::*; mod seq_launch; pub use self::seq_launch::*; mod seq_model; pub use self::seq_model::*; mod seq_view; pub use self::seq_view::*; #[cfg(test)] #[test] pub fn test_midi_clip () { + use crate::*; let clip = MidiClip::stop_all(); println!("{clip:?}"); @@ -22,6 +21,7 @@ mod seq_view; pub use self::seq_view::*; } #[cfg(test)] #[test] fn test_midi_play () { + use crate::*; let sequencer = Sequencer::default(); println!("{sequencer:?}"); } diff --git a/crates/device/src/sequencer/seq_model.rs b/crates/device/src/sequencer/seq_model.rs index db65cd55..c55a839a 100644 --- a/crates/device/src/sequencer/seq_model.rs +++ b/crates/device/src/sequencer/seq_model.rs @@ -1,6 +1,5 @@ //! MIDI sequencer use crate::*; -use tek_engine::jack::*; pub trait HasSequencer { fn sequencer (&self) -> &impl MidiPlayerApi; @@ -98,7 +97,7 @@ impl Sequencer { } impl std::fmt::Debug for Sequencer { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Sequencer") .field("clock", &self.clock) .field("play_clip", &self.play_clip) From 7690549bdc3017cc1314f718bff1b5f3e51b9422 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 11 May 2025 00:25:04 +0300 Subject: [PATCH 08/15] groovebox: display input meters! --- config/config_groovebox.edn | 5 +- crates/app/src/view.rs | 3 + crates/device/Cargo.toml | 3 +- crates/device/src/device.rs | 2 +- crates/device/src/lib.rs | 3 + crates/device/src/meter.rs | 40 ++++-- crates/device/src/mixer.rs | 41 +++++++ crates/device/src/sampler/sampler_api.rs | 10 +- crates/device/src/sampler/sampler_audio.rs | 134 ++++++++++++--------- crates/device/src/sampler/sampler_data.rs | 4 + crates/device/src/sampler/sampler_model.rs | 105 +++++++++------- crates/device/src/sampler/sampler_view.rs | 6 + 12 files changed, 239 insertions(+), 117 deletions(-) create mode 100644 crates/device/src/mixer.rs diff --git a/config/config_groovebox.edn b/config/config_groovebox.edn index 59bc032a..05f22924 100644 --- a/config/config_groovebox.edn +++ b/config/config_groovebox.edn @@ -8,8 +8,9 @@ (bsp/n (fixed/y 1 :view-status) (bsp/n (fixed/y 5 :view-sample-viewer) (bsp/w (fixed/x :w-sidebar :view-pool) - (bsp/e :view-samples-keys - (fill/y :view-editor)))))))) + (bsp/e :view-meters-input + (bsp/e :view-samples-keys + (fill/y :view-editor))))))))) (keys (layer-if :focus-pool-import "./keys_pool_file.edn") diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index 9333d10a..e81cb73d 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -56,6 +56,9 @@ impl App { ))) )) } + pub fn view_meters_input (&self) -> impl Content + use<'_> { + self.sampler().map(|s|s.view_meters_input()) + } } impl App { diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml index 0f8b2230..a6529854 100644 --- a/crates/device/Cargo.toml +++ b/crates/device/Cargo.toml @@ -20,8 +20,9 @@ default = [ "clock", "editor", "sequencer", "sampler", "lv2" ] clock = [] editor = [] meter = [] +mixer = [] sequencer = [ "clock", "uuid" ] -sampler = [ "meter", "symphonia", "wavers" ] +sampler = [ "meter", "mixer", "symphonia", "wavers" ] lv2 = [ "livi", "winit" ] vst2 = [] vst3 = [] diff --git a/crates/device/src/device.rs b/crates/device/src/device.rs index ee6a404f..70da6c28 100644 --- a/crates/device/src/device.rs +++ b/crates/device/src/device.rs @@ -31,7 +31,7 @@ audio!(|self: DeviceAudio<'a>, client, scope|{ use Device::*; match self.0 { #[cfg(feature = "sampler")] - Sampler(sampler) => SamplerAudio(sampler).process(client, scope), + Sampler(sampler) => sampler.process(client, scope), #[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope), diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index f15f3a7a..bb773305 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -35,6 +35,9 @@ pub use self::device::*; #[cfg(feature = "meter")] mod meter; #[cfg(feature = "meter")] pub use self::meter::*; +#[cfg(feature = "mixer")] mod mixer; +#[cfg(feature = "mixer")] pub use self::mixer::*; + #[cfg(feature = "lv2")] mod lv2; #[cfg(feature = "lv2")] pub use self::lv2::*; diff --git a/crates/device/src/meter.rs b/crates/device/src/meter.rs index 74dba443..81416bd0 100644 --- a/crates/device/src/meter.rs +++ b/crates/device/src/meter.rs @@ -1,16 +1,36 @@ use crate::*; -#[derive(Debug, Default, Clone)] -pub struct RMSMeter(f32); - -impl RMSMeter { - pub fn set (&mut self, samples: &[u32]) { - let sum: usize = samples.iter().map(|s|*s as usize).reduce(|sum, sample|sum + sample) - .unwrap_or(0); - self.0 = (sum as f32 / samples.len() as f32).sqrt(); - } +#[derive(Debug, Default)] +pub enum MeteringMode { + #[default] + Rms, + Log10, } -render!(TuiOut: |self: RMSMeter, to| { +#[derive(Debug, Default, Clone)] +pub struct Meter(pub f32); + +render!(TuiOut: |self: Meter, to| { let [x, y, w, h] = to.area(); + let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32 / 100.0).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2 - y, Some(Style::default().green())); + } + } }); + +pub fn to_rms (samples: &[u32]) -> f32 { + let sum: usize = samples.iter() + .map(|s|*s as usize) + .reduce(|sum, sample|sum + sample) + .unwrap_or(0); + (sum as f32 / samples.len() as f32).sqrt() +} + +pub fn to_log10 (samples: &[u32]) -> f32 { + 0.0 +} diff --git a/crates/device/src/mixer.rs b/crates/device/src/mixer.rs new file mode 100644 index 00000000..d6c34432 --- /dev/null +++ b/crates/device/src/mixer.rs @@ -0,0 +1,41 @@ +#[derive(Debug, Default)] +pub enum MixingMode { + #[default] + Summing, + Average, +} + +pub fn mix_summing ( + buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, +) -> bool { + let channels = buffer.len(); + for index in 0..frames { + if let Some(frame) = next() { + for (channel, sample) in frame.iter().enumerate() { + let channel = channel % channels; + buffer[channel][index] += sample * gain; + } + } else { + return false + } + } + true +} + +pub fn mix_average ( + buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, +) -> bool { + let channels = buffer.len(); + for index in 0..frames { + if let Some(frame) = next() { + for (channel, sample) in frame.iter().enumerate() { + let channel = channel % channels; + let value = buffer[channel][index]; + buffer[channel][index] = (value + sample * gain) / 2.0; + } + } else { + return false + } + } + true +} diff --git a/crates/device/src/sampler/sampler_api.rs b/crates/device/src/sampler/sampler_api.rs index e374f88e..fd120976 100644 --- a/crates/device/src/sampler/sampler_api.rs +++ b/crates/device/src/sampler/sampler_api.rs @@ -9,7 +9,7 @@ impl Sampler { //todo!(); //} ///// Immutable reference to sample at cursor. - //fn sample_selected (&self) -> MaybeSample { + //fn sample_selected (&self) -> Option>> { //for (i, sample) in self.mapped.iter().enumerate() { //if i == self.cursor().0 { //return sample.as_ref() @@ -88,7 +88,7 @@ impl SamplerCommand { //Self::Select(state.set_note_pos(i)) //} ///// Assign sample to pitch - //fn set (&self, pitch: u7, sample: MaybeSample) -> Option { + //fn set (&self, pitch: u7, sample: Option>>) -> Option { //let i = pitch.as_int() as usize; //let old = self.mapped[i].clone(); //self.mapped[i] = sample; @@ -106,7 +106,7 @@ impl SamplerCommand { //fn note_off (&self, state: &mut Sampler, pitch: u7) -> Option { //todo!() //} - //fn set_sample (&self, state: &mut Sampler, pitch: u7, s: MaybeSample) -> Option { + //fn set_sample (&self, state: &mut Sampler, pitch: u7, s: Option>>) -> Option { //Some(Self::SetSample(p, state.set_sample(p, s))) //} //fn import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option { @@ -131,7 +131,7 @@ impl SamplerCommand { ////(SetGain [p: u7, gain: f32] cmd_todo!("\n\rtodo: {self:?}")) ////(NoteOn [p: u7, velocity: u7] cmd_todo!("\n\rtodo: {self:?}")) ////(NoteOff [p: u7] cmd_todo!("\n\rtodo: {self:?}")) - ////(SetSample [p: u7, s: MaybeSample] Some(Self::SetSample(p, state.set_sample(p, s)))) + ////(SetSample [p: u7, s: Option>>] Some(Self::SetSample(p, state.set_sample(p, s)))) ////(Import [c: FileBrowserCommand] match c { ////FileBrowserCommand::Begin => { //////let voices = &state.state.voices; @@ -154,7 +154,7 @@ impl SamplerCommand { ////Some(Self::RecordCancel)) ////("record/finish" [] ////Some(Self::RecordFinish)) - ////("set/sample" [i: u7, s: MaybeSample] + ////("set/sample" [i: u7, s: Option>>] ////Some(Self::SetSample(i.expect("no index"), s.expect("no sampler")))) ////("set/start" [i: u7, s: usize] ////Some(Self::SetStart(i.expect("no index"), s.expect("no start")))) diff --git a/crates/device/src/sampler/sampler_audio.rs b/crates/device/src/sampler/sampler_audio.rs index 4cea762c..cfbe7b6e 100644 --- a/crates/device/src/sampler/sampler_audio.rs +++ b/crates/device/src/sampler/sampler_audio.rs @@ -1,80 +1,102 @@ use crate::*; -pub struct SamplerAudio<'a>(pub &'a mut Sampler); - -audio!(|self: SamplerAudio<'a>, _client, scope|{ - self.0.process_midi_in(scope); - self.0.clear_output_buffer(); - self.0.process_audio_out(scope); - self.0.write_output_buffer(scope); - self.0.process_audio_in(scope); +audio!(|self: Sampler, _client, scope|{ + self.process_midi_in(scope); + self.process_audio_out(scope); + self.process_audio_in(scope); Control::Continue }); impl Sampler { pub fn process_audio_in (&mut self, scope: &ProcessScope) { - let Sampler { audio_ins, input_meter, recording, .. } = self; - if audio_ins.len() != input_meter.len() { - *input_meter = vec![0.0;audio_ins.len()]; - } - if let Some((_, sample)) = recording { - let mut sample = sample.write().unwrap(); - if sample.channels.len() != audio_ins.len() { - panic!("channel count mismatch"); - } - let iterator = audio_ins.iter().zip(input_meter).zip(sample.channels.iter_mut()); - let mut length = 0; - for ((input, meter), channel) in iterator { - let slice = input.port().as_slice(scope); - length = length.max(slice.len()); - let total: f32 = slice.iter().map(|x|x.abs()).sum(); - let count = slice.len() as f32; - *meter = 10. * (total / count).log10(); - channel.extend_from_slice(slice); - } - sample.end += length; + self.reset_input_meters(); + if self.recording.is_some() { + self.record_into(scope); } else { - for (input, meter) in audio_ins.iter().zip(input_meter) { - let slice = input.port().as_slice(scope); - let total: f32 = slice.iter().map(|x|x.abs()).sum(); - let count = slice.len() as f32; - *meter = 10. * (total / count).log10(); - } + self.update_input_meters(scope); } } - /// Zero the output buffer. - pub fn clear_output_buffer (&mut self) { - for buffer in self.buffer.iter_mut() { - buffer.fill(0.0); + /// Make sure that input meter count corresponds to input channel count + fn reset_input_meters (&mut self) { + let channels = self.audio_ins.len(); + if self.input_meters.len() != channels { + self.input_meters = vec![f32::MIN;channels]; + } + } + + /// Record from inputs to sample + fn record_into (&mut self, scope: &ProcessScope) { + let mut sample = self.recording + .as_mut().expect("no recording sample").1 + .write().unwrap(); + if sample.channels.len() != self.audio_ins.len() { + panic!("channel count mismatch"); + } + let samples_with_meters = self.audio_ins.iter() + .zip(self.input_meters.iter_mut()) + .zip(sample.channels.iter_mut()); + let mut length = 0; + for ((input, meter), channel) in samples_with_meters { + let slice = input.port().as_slice(scope); + length = length.max(slice.len()); + let total: f32 = slice.iter().map(|x|x.abs()).sum(); + let count = slice.len() as f32; + *meter = 10. * (total / count).log10(); + channel.extend_from_slice(slice); + } + sample.end += length; + } + + /// Update input meters + fn update_input_meters (&mut self, scope: &ProcessScope) { + for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { + let slice = input.port().as_slice(scope); + let total: f32 = slice.iter().map(|x|x.abs()).sum(); + let count = slice.len() as f32; + *meter = 10. * (total / count).log10(); + } + } + + /// Make sure that output meter count corresponds to input channel count + fn reset_output_meters (&mut self) { + let channels = self.audio_outs.len(); + if self.output_meters.len() != channels { + self.output_meters = vec![f32::MIN;channels]; } } /// Mix all currently playing samples into the output. pub fn process_audio_out (&mut self, scope: &ProcessScope) { - let Sampler { ref mut buffer, voices, output_gain, .. } = self; + self.clear_output_buffer(); + self.populate_output_buffer(scope.n_frames() as usize); + self.write_output_buffer(scope); + } + + /// Zero the output buffer. + fn clear_output_buffer (&mut self) { + for buffer in self.buffer.iter_mut() { + buffer.fill(0.0); + } + } + + /// Write playing voices to output buffer + fn populate_output_buffer (&mut self, frames: usize) { + let Sampler { ref mut buffer, voices, output_gain, mixing_mode, .. } = self; let channel_count = buffer.len(); - voices.write().unwrap().retain_mut(|voice|{ - for index in 0..scope.n_frames() as usize { - if let Some(frame) = voice.next() { - for (channel, sample) in frame.iter().enumerate() { - // Averaging mixer: - //self.buffer[channel % channel_count][index] = ( - //(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0 - //); - buffer[channel % channel_count][index] += sample * *output_gain; - } - } else { - return false - } - } - true - }); + match mixing_mode { + MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{ + mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) + }), + MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{ + mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) + }), + } } /// Write output buffer to output ports. - pub fn write_output_buffer (&mut self, scope: &ProcessScope) { + fn write_output_buffer (&mut self, scope: &ProcessScope) { let Sampler { ref mut audio_outs, buffer, .. } = self; for (i, port) in audio_outs.iter_mut().enumerate() { let buffer = &buffer[i]; diff --git a/crates/device/src/sampler/sampler_data.rs b/crates/device/src/sampler/sampler_data.rs index 8c3b3bda..b4834174 100644 --- a/crates/device/src/sampler/sampler_data.rs +++ b/crates/device/src/sampler/sampler_data.rs @@ -1,6 +1,7 @@ use crate::*; impl Sample { + /// Read WAV from file pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { let mut channels: Vec> = vec![]; @@ -16,6 +17,7 @@ impl Sample { } Ok((end, data)) } + pub fn from_file (path: &PathBuf) -> Usually { let name = path.file_name().unwrap().to_string_lossy().into(); let mut sample = Self { name, ..Default::default() }; @@ -49,6 +51,7 @@ impl Sample { sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); Ok(sample) } + fn decode_packet ( &mut self, decoder: &mut Box, packet: Packet ) -> Usually<()> { @@ -84,4 +87,5 @@ impl Sample { } Ok(()) } + } diff --git a/crates/device/src/sampler/sampler_model.rs b/crates/device/src/sampler/sampler_model.rs index 2a55e143..ad429a9d 100644 --- a/crates/device/src/sampler/sampler_model.rs +++ b/crates/device/src/sampler/sampler_model.rs @@ -1,55 +1,76 @@ use crate::*; -pub type MaybeSample = Option>>; - /// The sampler device plays sounds in response to MIDI notes. #[derive(Debug)] pub struct Sampler { - pub name: String, - pub mapped: [MaybeSample;128], - pub recording: Option<(usize, Arc>)>, - pub unmapped: Vec>>, - pub voices: Arc>>, - pub midi_in: Option, - pub audio_ins: Vec, - pub input_meter: Vec, - pub audio_outs: Vec, - pub buffer: Vec>, - pub output_gain: f32, - pub editing: MaybeSample, - pub mode: Option, - /// Size of actual notes area - pub size: Measure, - /// Lowest note displayed - pub note_lo: AtomicUsize, - /// Selected note - pub note_pt: AtomicUsize, - /// Selected note as row/col - pub cursor: (AtomicUsize, AtomicUsize), - pub color: ItemTheme + /// Name of sampler. + pub name: String, + /// Device color. + pub color: ItemTheme, + /// Audio input ports. Samples get recorded here. + pub audio_ins: Vec, + /// Audio input meters. + pub input_meters: Vec, + /// Sample currently being recorded. + pub recording: Option<(usize, Arc>)>, + /// Recording buffer. + pub buffer: Vec>, + /// Samples mapped to MIDI notes. + pub mapped: [Option>>;128], + /// Samples that are not mapped to MIDI notes. + pub unmapped: Vec>>, + /// Sample currently being edited. + pub editing: Option>>, + /// MIDI input port. Triggers sample playback. + pub midi_in: Option, + /// Collection of currently playing instances of samples. + pub voices: Arc>>, + /// Audio output ports. Voices get played here. + pub audio_outs: Vec, + /// Audio output meters. + pub output_meters: Vec, + /// How to mix the voices. + pub mixing_mode: MixingMode, + /// How to meter the inputs and outputs. + pub metering_mode: MeteringMode, + /// Fixed gain applied to all output. + pub output_gain: f32, + /// Currently active modal, if any. + pub mode: Option, + /// Size of rendered sampler. + pub size: Measure, + /// Lowest note displayed. + pub note_lo: AtomicUsize, + /// Currently selected note. + pub note_pt: AtomicUsize, + /// Selected note as row/col. + pub cursor: (AtomicUsize, AtomicUsize), } impl Default for Sampler { fn default () -> Self { Self { - midi_in: None, - audio_ins: vec![], - input_meter: vec![0.0;2], - audio_outs: vec![], - name: "tek_sampler".to_string(), - mapped: [const { None };128], - unmapped: vec![], - voices: Arc::new(RwLock::new(vec![])), - buffer: vec![vec![0.0;16384];2], - output_gain: 1., - recording: None, - mode: None, - editing: None, - size: Default::default(), - note_lo: 0.into(), - note_pt: 0.into(), - cursor: (0.into(), 0.into()), - color: Default::default(), + midi_in: None, + audio_ins: vec![], + input_meters: vec![0.0;2], + output_meters: vec![0.0;2], + audio_outs: vec![], + name: "tek_sampler".to_string(), + mapped: [const { None };128], + unmapped: vec![], + voices: Arc::new(RwLock::new(vec![])), + buffer: vec![vec![0.0;16384];2], + output_gain: 1., + recording: None, + mode: None, + editing: None, + size: Default::default(), + note_lo: 0.into(), + note_pt: 0.into(), + cursor: (0.into(), 0.into()), + color: Default::default(), + mixing_mode: Default::default(), + metering_mode: Default::default(), } } } diff --git a/crates/device/src/sampler/sampler_view.rs b/crates/device/src/sampler/sampler_view.rs index d984b3a4..8ceef605 100644 --- a/crates/device/src/sampler/sampler_view.rs +++ b/crates/device/src/sampler/sampler_view.rs @@ -102,6 +102,12 @@ impl Sampler { pub fn status (&self, index: usize) -> impl Content { draw_status(self.mapped[index].as_ref()) } + + pub fn view_meters_input (&self) -> impl Content + use<'_> { + Tui::bg(Black, Fixed::x(2, Map::east(1, ||self.input_meters.iter(), |value, _index|{ + Fill::y(Meter(*value)) + }))) + } } fn draw_list_item (sample: &Option>>) -> String { From 7bc37e76596ebba792325ba178dc14bc23b27344 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 11 May 2025 00:40:53 +0300 Subject: [PATCH 09/15] groovebox: add slots for output meters --- config/config_groovebox.edn | 17 +++++++++-------- crates/app/src/view.rs | 3 +++ crates/device/src/sampler/sampler_model.rs | 4 ++-- crates/device/src/sampler/sampler_view.rs | 6 ++++++ 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/config/config_groovebox.edn b/config/config_groovebox.edn index 05f22924..24154b74 100644 --- a/config/config_groovebox.edn +++ b/config/config_groovebox.edn @@ -3,14 +3,15 @@ (info "A sequencer with built-in sampler.") (view - (bsp/a :view-dialog - (bsp/s (fixed/y 1 :view-transport) - (bsp/n (fixed/y 1 :view-status) - (bsp/n (fixed/y 5 :view-sample-viewer) - (bsp/w (fixed/x :w-sidebar :view-pool) - (bsp/e :view-meters-input - (bsp/e :view-samples-keys - (fill/y :view-editor))))))))) + (bsp/a :view-dialog + (bsp/s (fixed/y 1 :view-transport) + (bsp/n (fixed/y 1 :view-status) + (bsp/w :view-meters-output + (bsp/e :view-meters-input + (bsp/n (fixed/y 5 :view-sample-viewer) + (bsp/w (fixed/x :w-sidebar :view-pool) + (bsp/e :view-samples-keys + (fill/y :view-editor)))))))))) (keys (layer-if :focus-pool-import "./keys_pool_file.edn") diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index e81cb73d..4a2c7859 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -59,6 +59,9 @@ impl App { pub fn view_meters_input (&self) -> impl Content + use<'_> { self.sampler().map(|s|s.view_meters_input()) } + pub fn view_meters_output (&self) -> impl Content + use<'_> { + self.sampler().map(|s|s.view_meters_output()) + } } impl App { diff --git a/crates/device/src/sampler/sampler_model.rs b/crates/device/src/sampler/sampler_model.rs index ad429a9d..d51d106a 100644 --- a/crates/device/src/sampler/sampler_model.rs +++ b/crates/device/src/sampler/sampler_model.rs @@ -52,8 +52,8 @@ impl Default for Sampler { Self { midi_in: None, audio_ins: vec![], - input_meters: vec![0.0;2], - output_meters: vec![0.0;2], + input_meters: vec![f32::MIN;2], + output_meters: vec![f32::MIN;2], audio_outs: vec![], name: "tek_sampler".to_string(), mapped: [const { None };128], diff --git a/crates/device/src/sampler/sampler_view.rs b/crates/device/src/sampler/sampler_view.rs index 8ceef605..c73b4908 100644 --- a/crates/device/src/sampler/sampler_view.rs +++ b/crates/device/src/sampler/sampler_view.rs @@ -108,6 +108,12 @@ impl Sampler { Fill::y(Meter(*value)) }))) } + + pub fn view_meters_output (&self) -> impl Content + use<'_> { + Tui::bg(Black, Fixed::x(2, Map::east(1, ||self.output_meters.iter(), |value, _index|{ + Fill::y(Meter(*value)) + }))) + } } fn draw_list_item (sample: &Option>>) -> String { From 6db5df5210287b3e77e9e6086cd36c1422160082 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 11 May 2025 00:45:48 +0300 Subject: [PATCH 10/15] meter: extract to_log10, fix types of to_rms --- crates/device/src/meter.rs | 18 ++++++++++-------- crates/device/src/sampler/sampler_audio.rs | 8 ++------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/crates/device/src/meter.rs b/crates/device/src/meter.rs index 81416bd0..e791aefc 100644 --- a/crates/device/src/meter.rs +++ b/crates/device/src/meter.rs @@ -15,7 +15,7 @@ render!(TuiOut: |self: Meter, to| { let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); let v = (signal * h as f32 / 100.0).ceil() as u16; let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); for y in y..(y + v) { for x in x..(x + w) { to.blit(&"▌", x, y2 - y, Some(Style::default().green())); @@ -23,14 +23,16 @@ render!(TuiOut: |self: Meter, to| { } }); -pub fn to_rms (samples: &[u32]) -> f32 { - let sum: usize = samples.iter() - .map(|s|*s as usize) +pub fn to_rms (samples: &[f32]) -> f32 { + let sum = samples.iter() + .map(|s|*s) .reduce(|sum, sample|sum + sample) - .unwrap_or(0); - (sum as f32 / samples.len() as f32).sqrt() + .unwrap_or(0.0); + (sum / samples.len() as f32).sqrt() } -pub fn to_log10 (samples: &[u32]) -> f32 { - 0.0 +pub fn to_log10 (samples: &[f32]) -> f32 { + let total: f32 = samples.iter().map(|x|x.abs()).sum(); + let count = samples.len() as f32; + 10. * (total / count).log10() } diff --git a/crates/device/src/sampler/sampler_audio.rs b/crates/device/src/sampler/sampler_audio.rs index cfbe7b6e..64c7ba82 100644 --- a/crates/device/src/sampler/sampler_audio.rs +++ b/crates/device/src/sampler/sampler_audio.rs @@ -41,9 +41,7 @@ impl Sampler { for ((input, meter), channel) in samples_with_meters { let slice = input.port().as_slice(scope); length = length.max(slice.len()); - let total: f32 = slice.iter().map(|x|x.abs()).sum(); - let count = slice.len() as f32; - *meter = 10. * (total / count).log10(); + *meter = to_log10(slice); channel.extend_from_slice(slice); } sample.end += length; @@ -53,9 +51,7 @@ impl Sampler { fn update_input_meters (&mut self, scope: &ProcessScope) { for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { let slice = input.port().as_slice(scope); - let total: f32 = slice.iter().map(|x|x.abs()).sum(); - let count = slice.len() as f32; - *meter = 10. * (total / count).log10(); + *meter = to_log10(slice); } } From b9c101081b829f6101ed3c9b746ec723377832a8 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 11 May 2025 01:28:05 +0300 Subject: [PATCH 11/15] sampler, meter: switch to rms; reenable viewer --- crates/device/src/meter.rs | 38 +++++++++++++++------- crates/device/src/sampler/sampler_api.rs | 9 ++--- crates/device/src/sampler/sampler_audio.rs | 4 +-- crates/device/src/sampler/sampler_model.rs | 4 +-- crates/device/src/sampler/sampler_view.rs | 21 ++++++------ 5 files changed, 45 insertions(+), 31 deletions(-) diff --git a/crates/device/src/meter.rs b/crates/device/src/meter.rs index e791aefc..0c2e749a 100644 --- a/crates/device/src/meter.rs +++ b/crates/device/src/meter.rs @@ -8,14 +8,14 @@ pub enum MeteringMode { } #[derive(Debug, Default, Clone)] -pub struct Meter(pub f32); +pub struct Log10Meter(pub f32); -render!(TuiOut: |self: Meter, to| { +render!(TuiOut: |self: Log10Meter, to| { let [x, y, w, h] = to.area(); let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); let v = (signal * h as f32 / 100.0).ceil() as u16; let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); for y in y..(y + v) { for x in x..(x + w) { to.blit(&"▌", x, y2 - y, Some(Style::default().green())); @@ -23,16 +23,32 @@ render!(TuiOut: |self: Meter, to| { } }); -pub fn to_rms (samples: &[f32]) -> f32 { - let sum = samples.iter() - .map(|s|*s) - .reduce(|sum, sample|sum + sample) - .unwrap_or(0.0); - (sum / samples.len() as f32).sqrt() -} - pub fn to_log10 (samples: &[f32]) -> f32 { let total: f32 = samples.iter().map(|x|x.abs()).sum(); let count = samples.len() as f32; 10. * (total / count).log10() } + +#[derive(Debug, Default, Clone)] +pub struct RmsMeter(pub f32); + +render!(TuiOut: |self: RmsMeter, to| { + let [x, y, w, h] = to.area(); + let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); + } + } +}); + +pub fn to_rms (samples: &[f32]) -> f32 { + let sum = samples.iter() + .map(|s|*s) + .reduce(|sum, sample|sum + sample.abs()) + .unwrap_or(0.0); + (sum / samples.len() as f32).sqrt() +} diff --git a/crates/device/src/sampler/sampler_api.rs b/crates/device/src/sampler/sampler_api.rs index fd120976..f490350e 100644 --- a/crates/device/src/sampler/sampler_api.rs +++ b/crates/device/src/sampler/sampler_api.rs @@ -57,14 +57,11 @@ impl SamplerCommand { Self::record_begin(sampler, sample) } } - fn record_begin (sampler: &mut Sampler, sample: usize) -> Perhaps { + fn record_begin (sampler: &mut Sampler, pitch: usize) -> Perhaps { sampler.recording = Some(( - sample, + pitch, Arc::new(RwLock::new(Sample::new( - "Sample", - 0, - 0, - vec![vec![];sampler.audio_ins.len()] + "Sample", 0, 0, vec![vec![];sampler.audio_ins.len()] ))) )); Ok(None) diff --git a/crates/device/src/sampler/sampler_audio.rs b/crates/device/src/sampler/sampler_audio.rs index 64c7ba82..87d0a78e 100644 --- a/crates/device/src/sampler/sampler_audio.rs +++ b/crates/device/src/sampler/sampler_audio.rs @@ -41,7 +41,7 @@ impl Sampler { for ((input, meter), channel) in samples_with_meters { let slice = input.port().as_slice(scope); length = length.max(slice.len()); - *meter = to_log10(slice); + *meter = to_rms(slice); channel.extend_from_slice(slice); } sample.end += length; @@ -51,7 +51,7 @@ impl Sampler { fn update_input_meters (&mut self, scope: &ProcessScope) { for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { let slice = input.port().as_slice(scope); - *meter = to_log10(slice); + *meter = to_rms(slice); } } diff --git a/crates/device/src/sampler/sampler_model.rs b/crates/device/src/sampler/sampler_model.rs index d51d106a..ad429a9d 100644 --- a/crates/device/src/sampler/sampler_model.rs +++ b/crates/device/src/sampler/sampler_model.rs @@ -52,8 +52,8 @@ impl Default for Sampler { Self { midi_in: None, audio_ins: vec![], - input_meters: vec![f32::MIN;2], - output_meters: vec![f32::MIN;2], + input_meters: vec![0.0;2], + output_meters: vec![0.0;2], audio_outs: vec![], name: "tek_sampler".to_string(), mapped: [const { None };128], diff --git a/crates/device/src/sampler/sampler_view.rs b/crates/device/src/sampler/sampler_view.rs index c73b4908..63647804 100644 --- a/crates/device/src/sampler/sampler_view.rs +++ b/crates/device/src/sampler/sampler_view.rs @@ -90,13 +90,14 @@ impl Sampler { } pub fn view_sample (&self, note_pt: usize) -> impl Content + use<'_> { - Outer(true, Style::default().fg(Tui::g(96))).enclose(draw_viewer(if let Some((_, sample)) = &self.recording { - Some(sample) - } else if let Some(sample) = &self.mapped[note_pt] { - Some(sample) - } else { - None - })) + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::xy(draw_viewer(if let Some((_, sample)) = &self.recording { + Some(sample) + } else if let Some(sample) = &self.mapped[note_pt] { + Some(sample) + } else { + None + }))) } pub fn status (&self, index: usize) -> impl Content { @@ -105,13 +106,13 @@ impl Sampler { pub fn view_meters_input (&self) -> impl Content + use<'_> { Tui::bg(Black, Fixed::x(2, Map::east(1, ||self.input_meters.iter(), |value, _index|{ - Fill::y(Meter(*value)) + Fill::y(RmsMeter(*value)) }))) } pub fn view_meters_output (&self) -> impl Content + use<'_> { Tui::bg(Black, Fixed::x(2, Map::east(1, ||self.output_meters.iter(), |value, _index|{ - Fill::y(Meter(*value)) + Fill::y(RmsMeter(*value)) }))) } } @@ -132,7 +133,7 @@ fn draw_list_item (sample: &Option>>) -> String { } fn draw_viewer (sample: Option<&Arc>>) -> impl Content + use<'_> { - let min_db = -40.0; + let min_db = -64.0; ThunkRender::new(move|to: &mut TuiOut|{ let [x, y, width, height] = to.area(); let area = Rect { x, y, width, height }; From ee2efd1c26e16584be179c76e414257d4483bd1e Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 11 May 2025 01:36:14 +0300 Subject: [PATCH 12/15] sampler: replace red x with record instruction --- crates/device/src/sampler/sampler_view.rs | 86 +++++++++++++---------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/crates/device/src/sampler/sampler_view.rs b/crates/device/src/sampler/sampler_view.rs index 63647804..b8fb9b20 100644 --- a/crates/device/src/sampler/sampler_view.rs +++ b/crates/device/src/sampler/sampler_view.rs @@ -137,45 +137,53 @@ fn draw_viewer (sample: Option<&Arc>>) -> impl Content + ThunkRender::new(move|to: &mut TuiOut|{ let [x, y, width, height] = to.area(); let area = Rect { x, y, width, height }; - let (x_bounds, y_bounds, lines): ([f64;2], [f64;2], Vec) = - if let Some(sample) = &sample { - let sample = sample.read().unwrap(); - let start = sample.start as f64; - let end = sample.end as f64; - let length = end - start; - let step = length / width as f64; - let mut t = start; - let mut lines = vec![]; - while t < end { - let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; - let total: f32 = chunk.iter().map(|x|x.abs()).sum(); - let count = chunk.len() as f32; - let meter = 10. * (total / count).log10(); - let x = t as f64; - let y = meter as f64; - lines.push(Line::new(x, min_db, x, y, Color::Green)); - t += step / 2.; - } - ( - [sample.start as f64, sample.end as f64], - [min_db, 0.], - lines - ) - } else { - ( - [0.0, width as f64], - [0.0, height as f64], - vec![ - Line::new(0.0, 0.0, width as f64, height as f64, Color::Red), - Line::new(width as f64, 0.0, 0.0, height as f64, Color::Red), - ] - ) - }; - Canvas::default() - .x_bounds(x_bounds) - .y_bounds(y_bounds) - .paint(|ctx| { for line in lines.iter() { ctx.draw(line) } }) - .render(area, &mut to.buffer); + if let Some(sample) = &sample { + let sample = sample.read().unwrap(); + let start = sample.start as f64; + let end = sample.end as f64; + let length = end - start; + let step = length / width as f64; + let mut t = start; + let mut lines = vec![]; + while t < end { + let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; + let total: f32 = chunk.iter().map(|x|x.abs()).sum(); + let count = chunk.len() as f32; + let meter = 10. * (total / count).log10(); + let x = t as f64; + let y = meter as f64; + lines.push(Line::new(x, min_db, x, y, Color::Green)); + t += step / 2.; + } + Canvas::default() + .x_bounds([sample.start as f64, sample.end as f64]) + .y_bounds([min_db, 0.]) + .paint(|ctx| { + for line in lines.iter() { + ctx.draw(line); + } + //FIXME: proportions + //let text = "press record to finish sampling"; + //ctx.print( + //(width - text.len() as u16) as f64 / 2.0, + //height as f64 / 2.0, + //text.red() + //); + }).render(area, &mut to.buffer); + } else { + Canvas::default() + .x_bounds([0.0, width as f64]) + .y_bounds([0.0, height as f64]) + .paint(|ctx| { + let text = "press record to begin sampling"; + ctx.print( + (width - text.len() as u16) as f64 / 2.0, + height as f64 / 2.0, + text.red() + ); + }) + .render(area, &mut to.buffer); + } }) } From 836624674e8f2e84ccebdb5cbe1e10d16a66d238 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 11 May 2025 01:40:38 +0300 Subject: [PATCH 13/15] track: pass initial clip --- crates/app/src/model/track.rs | 6 ++++-- crates/cli/tek.rs | 9 ++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/app/src/model/track.rs b/crates/app/src/model/track.rs index 2d91244a..959238d8 100644 --- a/crates/app/src/model/track.rs +++ b/crates/app/src/model/track.rs @@ -28,6 +28,7 @@ impl Track { color: Option, jack: &Jack, clock: Option<&Clock>, + clip: Option<&Arc>>, midi_from: &[PortConnect], midi_to: &[PortConnect], ) -> Usually { @@ -38,7 +39,7 @@ impl Track { format!("{}/sequencer", name.as_ref()), jack, clock, - None, + clip, midi_from, midi_to )?, @@ -51,13 +52,14 @@ impl Track { color: Option, jack: &Jack, clock: Option<&Clock>, + clip: Option<&Arc>>, midi_from: &[PortConnect], midi_to: &[PortConnect], audio_from: &[&[PortConnect];2], audio_to: &[&[PortConnect];2], ) -> Usually { let mut track = Self::new( - name, color, jack, clock, midi_from, midi_to + name, color, jack, clock, clip, midi_from, midi_to )?; track.devices.push(Device::Sampler(Sampler::new( jack, diff --git a/crates/cli/tek.rs b/crates/cli/tek.rs index 6d185221..b65a8abf 100644 --- a/crates/cli/tek.rs +++ b/crates/cli/tek.rs @@ -84,9 +84,10 @@ impl Cli { let audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; let audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; let clip = match mode { - LaunchMode::Sequencer | LaunchMode::Groovebox => Some(Arc::new(RwLock::new(MidiClip::new( - "Clip", true, 384usize, None, Some(ItemColor::random().into())), - ))), + LaunchMode::Sequencer | LaunchMode::Groovebox => + Some(Arc::new(RwLock::new(MidiClip::new( + "Clip", true, 384usize, None, Some(ItemColor::random().into())), + ))), _ => None, }; let scenes = vec![]; @@ -141,6 +142,7 @@ impl Cli { None, jack, None, + clip.as_ref(), midi_froms.as_slice(), midi_tos.as_slice() )? @@ -151,6 +153,7 @@ impl Cli { None, jack, None, + clip.as_ref(), midi_froms.as_slice(), midi_froms.as_slice(), audio_froms, From 329da026d760551759de81ed83c09c21c77ce89e Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 11 May 2025 01:46:37 +0300 Subject: [PATCH 14/15] sequencer: extract seq_audio, remove Api suffix from traits --- crates/device/src/sequencer.rs | 1 + crates/device/src/sequencer/seq_audio.rs | 286 ++++++++++++++++++++++ crates/device/src/sequencer/seq_model.rs | 297 +---------------------- 3 files changed, 293 insertions(+), 291 deletions(-) create mode 100644 crates/device/src/sequencer/seq_audio.rs diff --git a/crates/device/src/sequencer.rs b/crates/device/src/sequencer.rs index df991b7f..3b6d9fee 100644 --- a/crates/device/src/sequencer.rs +++ b/crates/device/src/sequencer.rs @@ -1,3 +1,4 @@ +mod seq_audio; pub use self::seq_audio::*; mod seq_clip; pub use self::seq_clip::*; mod seq_launch; pub use self::seq_launch::*; mod seq_model; pub use self::seq_model::*; diff --git a/crates/device/src/sequencer/seq_audio.rs b/crates/device/src/sequencer/seq_audio.rs new file mode 100644 index 00000000..c4f05a6d --- /dev/null +++ b/crates/device/src/sequencer/seq_audio.rs @@ -0,0 +1,286 @@ +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> { + 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 model.has_midi_ins() { + if model.recording() || model.monitoring() { + // Record and/or monitor input + model.record(scope, midi_buf) + } else if model.has_midi_outs() && 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(); + } + + fn monitoring (&self) -> bool; + + fn monitoring_mut (&mut self) -> &mut bool; + + fn toggle_monitor (&mut self) { + *self.monitoring_mut() = !self.monitoring(); + } + + 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_mut().iter() { + for (sample, event, bytes) in parse_midi_input(input.port().iter(scope)) { + if let LiveEvent::Midi { message, .. } = event { + if monitoring { + midi_buf[sample].push(bytes.to_vec()); + } + // FIXME: don't lock on every event! + update_keys(&mut notes_in.write().unwrap(), &message); + } + } + } + } + + fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { + if self.monitoring() { + self.monitor(scope, midi_buf); + } + 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 record_clip ( + &mut self, + scope: &ProcessScope, + started: Moment, + clip: &Option>>, + _midi_buf: &mut Vec>> + ) { + if let Some(clip) = clip { + let sample0 = scope.last_frame_time() as usize; + let start = started.sample.get() as usize; + let _recording = self.recording(); + let timebase = self.clock().timebase().clone(); + let quant = self.clock().quant.get(); + let mut clip = clip.write().unwrap(); + let length = clip.length; + for input in self.midi_ins_mut().iter() { + for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) { + if let LiveEvent::Midi { message, .. } = event { + clip.record_event({ + let sample = (sample0 + sample - start) as f64; + let pulse = timebase.samples_to_pulse(sample); + let quantized = (pulse / quant).round() * quant; + quantized as usize % length + }, message); + } + } + } + } + } + + fn record_next (&mut self) { + // TODO switch to next clip and record into it + } + +} + +pub trait 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. + self.play_clip().as_ref().map_or(true, |(started, clip)|{ + self.play_chunk(scope, note_buf, out, started, clip) + }) + } + + /// 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_chunk ( + &self, + scope: &ProcessScope, + note_buf: &mut Vec, + out: &mut [Vec>], + started: &Moment, + clip: &Option>> + ) -> bool { + // First sample to populate. Greater than 0 means that the first + // pulse of the clip falls somewhere in the middle of the chunk. + let sample = (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 + ); + // Iterator that emits sample (index into output buffer at which to write MIDI event) + // paired with pulse (index into clip from which to take the MIDI event) for each + // sample of the output buffer that corresponds to a MIDI pulse. + let pulses = self.clock().timebase().pulses_between_samples(sample, sample + scope.n_frames() as usize); + // Notes active during current chunk. + let notes = &mut self.notes_out().write().unwrap(); + let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); + for (sample, pulse) in pulses { + // 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 }; + 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 + } + + 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 c55a839a..60a57b26 100644 --- a/crates/device/src/sequencer/seq_model.rs +++ b/crates/device/src/sequencer/seq_model.rs @@ -2,23 +2,19 @@ use crate::*; pub trait HasSequencer { - fn sequencer (&self) -> &impl MidiPlayerApi; - fn sequencer_mut (&mut self) -> &mut impl MidiPlayerApi; + fn sequencer (&self) -> &impl MidiSequencer; + fn sequencer_mut (&mut self) -> &mut impl MidiSequencer; } #[macro_export] macro_rules! has_sequencer { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? HasSequencer for $Struct $(<$($L),*$($T),*>)? { - fn sequencer (&$self) -> &impl MidiPlayerApi { &$cb } - fn sequencer_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb } + fn sequencer (&$self) -> &impl MidiSequencer { &$cb } + fn sequencer_mut (&mut $self) -> &mut impl MidiSequencer { &mut$cb } } } } -pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {} - -impl MidiPlayerApi for Sequencer {} - /// Contains state for playing a clip pub struct Sequencer { /// State of clock and playhead @@ -119,44 +115,7 @@ impl HasMidiOuts for Sequencer { fn midi_note (&mut self) -> &mut Vec { &mut self.note_buf } } -/// Hosts the JACK callback for a single MIDI sequencer -pub struct PlayerAudio<'a, T: MidiPlayerApi>( - /// 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> { - 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 model.has_midi_ins() { - if model.recording() || model.monitoring() { - // Record and/or monitor input - model.record(scope, midi_buf) - } else if model.has_midi_outs() && model.monitoring() { - // Monitor input to output - model.monitor(scope, midi_buf) - } - } - // Write to output port(s) - model.write(scope, midi_buf); - Control::Continue - } -} - -impl MidiRecordApi for Sequencer { +impl MidiRecorder for Sequencer { fn recording (&self) -> bool { self.recording } @@ -180,7 +139,7 @@ impl MidiRecordApi for Sequencer { } } -impl MidiPlaybackApi for Sequencer { +impl MidiPlayer for Sequencer { fn notes_out (&self) -> &Arc> { &self.notes_out } @@ -206,247 +165,3 @@ impl HasPlayClip for Sequencer { &mut self.next_clip } } - -pub trait MidiRecordApi: 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 monitoring (&self) -> bool; - - fn monitoring_mut (&mut self) -> &mut bool; - - fn toggle_monitor (&mut self) { - *self.monitoring_mut() = !self.monitoring(); - } - - 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_mut().iter() { - for (sample, event, bytes) in parse_midi_input(input.port().iter(scope)) { - if let LiveEvent::Midi { message, .. } = event { - if monitoring { - midi_buf[sample].push(bytes.to_vec()); - } - // FIXME: don't lock on every event! - update_keys(&mut notes_in.write().unwrap(), &message); - } - } - } - } - - fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { - if self.monitoring() { - self.monitor(scope, midi_buf); - } - 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 record_clip ( - &mut self, - scope: &ProcessScope, - started: Moment, - clip: &Option>>, - _midi_buf: &mut Vec>> - ) { - if let Some(clip) = clip { - let sample0 = scope.last_frame_time() as usize; - let start = started.sample.get() as usize; - let _recording = self.recording(); - let timebase = self.clock().timebase().clone(); - let quant = self.clock().quant.get(); - let mut clip = clip.write().unwrap(); - let length = clip.length; - for input in self.midi_ins_mut().iter() { - for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) { - if let LiveEvent::Midi { message, .. } = event { - clip.record_event({ - let sample = (sample0 + sample - start) as f64; - let pulse = timebase.samples_to_pulse(sample); - let quantized = (pulse / quant).round() * quant; - quantized as usize % length - }, message); - } - } - } - } - } - - fn record_next (&mut self) { - // TODO switch to next clip and record into it - } - -} - -pub trait MidiPlaybackApi: 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. - self.play_clip().as_ref().map_or(true, |(started, clip)|{ - self.play_chunk(scope, note_buf, out, started, clip) - }) - } - - /// 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_chunk ( - &self, - scope: &ProcessScope, - note_buf: &mut Vec, - out: &mut [Vec>], - started: &Moment, - clip: &Option>> - ) -> bool { - // First sample to populate. Greater than 0 means that the first - // pulse of the clip falls somewhere in the middle of the chunk. - let sample = (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 - ); - // Iterator that emits sample (index into output buffer at which to write MIDI event) - // paired with pulse (index into clip from which to take the MIDI event) for each - // sample of the output buffer that corresponds to a MIDI pulse. - let pulses = self.clock().timebase().pulses_between_samples(sample, sample + scope.n_frames() as usize); - // Notes active during current chunk. - let notes = &mut self.notes_out().write().unwrap(); - let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); - for (sample, pulse) in pulses { - // 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 }; - 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 - } - - 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:?}"); - }); - } - } - } -} From 997d67a4879330af8d2ccb0a30f7c0efeb7bb3ee Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 11 May 2025 01:56:02 +0300 Subject: [PATCH 15/15] sequencer: extract get_sample_offset, get_pulses --- crates/device/src/sequencer/seq_audio.rs | 95 ++++++++++++++---------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/crates/device/src/sequencer/seq_audio.rs b/crates/device/src/sequencer/seq_audio.rs index c4f05a6d..0b78b210 100644 --- a/crates/device/src/sequencer/seq_audio.rs +++ b/crates/device/src/sequencer/seq_audio.rs @@ -163,9 +163,64 @@ pub trait MidiPlayer: HasPlayClip + HasClock + HasMidiOuts { } // 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. - self.play_clip().as_ref().map_or(true, |(started, clip)|{ + 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. @@ -197,42 +252,6 @@ pub trait MidiPlayer: HasPlayClip + HasClock + HasMidiOuts { } } - fn play_chunk ( - &self, - scope: &ProcessScope, - note_buf: &mut Vec, - out: &mut [Vec>], - started: &Moment, - clip: &Option>> - ) -> bool { - // First sample to populate. Greater than 0 means that the first - // pulse of the clip falls somewhere in the middle of the chunk. - let sample = (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 - ); - // Iterator that emits sample (index into output buffer at which to write MIDI event) - // paired with pulse (index into clip from which to take the MIDI event) for each - // sample of the output buffer that corresponds to a MIDI pulse. - let pulses = self.clock().timebase().pulses_between_samples(sample, sample + scope.n_frames() as usize); - // Notes active during current chunk. - let notes = &mut self.notes_out().write().unwrap(); - let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); - for (sample, pulse) in pulses { - // 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 }; - 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 - } - fn play_pulse ( clip: &RwLock, pulse: usize,