From ad2f75bee66478f5e233182fa67c008c604e33ee Mon Sep 17 00:00:00 2001 From: unspeaker Date: Fri, 1 Nov 2024 02:15:51 +0200 Subject: [PATCH] wip: tying it together... --- crates/tek_core/src/time.rs | 89 ++++++++--------------- crates/tek_sequencer/src/arranger.rs | 14 +--- crates/tek_sequencer/src/arranger_tui.rs | 22 +++--- crates/tek_sequencer/src/sequencer.rs | 44 ++++++----- crates/tek_sequencer/src/sequencer_snd.rs | 7 +- crates/tek_sequencer/src/transport.rs | 13 ++-- crates/tek_sequencer/src/transport_tui.rs | 2 +- 7 files changed, 76 insertions(+), 115 deletions(-) diff --git a/crates/tek_core/src/time.rs b/crates/tek_core/src/time.rs index 49763bab..56635dc8 100644 --- a/crates/tek_core/src/time.rs +++ b/crates/tek_core/src/time.rs @@ -1,6 +1,5 @@ use crate::*; use std::iter::Iterator; - /// Any numeric type that represents time pub trait TimeUnit: Copy + Display + PartialOrd + PartialEq + Add + Mul @@ -8,15 +7,35 @@ pub trait TimeUnit: Copy + Display + PartialOrd + PartialEq impl TimeUnit for T where T: Copy + Display + PartialOrd + PartialEq + Add + Mul + Div + Rem {} - /// Integer time unit, such as samples, pulses, or microseconds pub trait TimeInteger: TimeUnit + From + Into + Copy {} impl TimeInteger for T where T: TimeUnit + From + Into + Copy {} - /// Floating time unit, such as beats or seconds pub trait TimeFloat: TimeUnit + From + Into + Copy {} impl TimeFloat for T where T: TimeUnit + From + Into + Copy {} - +/// Defines global temporal resolutions. +#[derive(Debug)] pub struct Timebase { + /// Audio samples per second + sr: AtomicF64, + /// MIDI beats per minute + bpm: AtomicF64, + /// MIDI ticks per beat + ppq: AtomicF64, +} +/// Represents a point in time in all scales +#[derive(Debug, Default)] pub struct Instant { + pub timebase: Arc, + /// Current time in microseconds + usec: AtomicUsize, + /// Current time in audio samples + sample: AtomicUsize, + /// Current time in MIDI pulses + pulse: AtomicF64, +} +/// Defines samples per tick. +pub struct Ticks(pub f64); +/// Iterator that emits subsequent ticks within a range. +pub struct TicksIterator(f64, usize, usize, usize); /// Trait for struct that defines a sample rate in hertz (samples per second) pub trait SampleRate { /// Get the sample rate @@ -40,7 +59,6 @@ pub trait SampleRate { usecs * self.sample_per_usec() } } - /// Trait for struct that defines a tempo in beats per minute pub trait BeatsPerMinute { /// Get the tempo @@ -79,7 +97,6 @@ pub trait BeatsPerMinute { events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() } } - /// Trait for struct that defines a MIDI resolution in pulses per quaver (beat) pub trait PulsesPerQuaver { const DEFAULT_PPQ: U; @@ -141,7 +158,6 @@ pub trait PulsesPerQuaver { { self.sr() / self.pulses_per_second() } - #[inline] fn format_beats (&self, pulse: U) -> String where U: TimeInteger { let ppq = self.ppq(); let (beats, pulses) = if ppq > U::from(0) { @@ -154,22 +170,19 @@ pub trait PulsesPerQuaver { format!("{bars}.{beats}.{pulses:02}") } } - pub trait SamplePosition { fn sample (&self) -> U; fn set_sample (&self, sample: U); } - pub trait PulsePosition { fn pulse (&self) -> U; fn set_pulse (&self, pulse: U); - #[inline] fn format_current_pulse (&self) -> String + #[inline] fn format_beat (&self) -> String where Self: PulsesPerQuaver, U: TimeInteger { self.format_beats(self.pulse()) } } - pub trait UsecPosition { fn usec (&self) -> U; fn set_usec (&self, usec: U); @@ -180,7 +193,6 @@ pub trait UsecPosition { format!("{minutes}:{seconds:02}:{msecs:03}") } } - pub trait LaunchSync { fn sync (&self) -> U; fn set_sync (&self, sync: U); @@ -194,27 +206,19 @@ pub trait LaunchSync { } } } - pub trait Quantize { fn quant (&self) -> T; fn set_quant (&self, quant: T); } - -#[derive(Debug)] -/// Defines global temporal resolutions. -pub struct Timebase { - /// Audio samples per second - pub sr: AtomicF64, - /// MIDI beats per minute - pub bpm: AtomicF64, - /// MIDI ticks per beat - pub ppq: AtomicF64, -} impl Default for Timebase { fn default () -> Self { Self::new(48000f64, 150f64, 96f64) } } impl Timebase { pub fn new (s: impl Into, b: impl Into, p: impl Into) -> Self { Self { sr: s.into(), bpm: b.into(), ppq: p.into() } } + /// Iterate over ticks between start and end. + pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator { + TicksIterator(self.samples_per_pulse(), start, start, end) + } } impl SampleRate for Timebase { #[inline] fn sr (&self) -> f64 { self.sr.load(Ordering::Relaxed) } @@ -229,18 +233,6 @@ impl PulsesPerQuaver for Timebase { #[inline] fn ppq (&self) -> f64 { self.ppq.load(Ordering::Relaxed) } #[inline] fn set_ppq (&self, ppq: f64) { self.ppq.store(ppq, Ordering::Relaxed); } } - -#[derive(Debug, Default)] -/// Represents a point in time in all scales -pub struct Instant { - timebase: Arc, - /// Current time in microseconds - usec: AtomicUsize, - /// Current time in audio samples - sample: AtomicUsize, - /// Current time in MIDI pulses - pulse: AtomicF64, -} impl SampleRate for Instant { #[inline] fn sr (&self) -> f64 { self.timebase.sr() } #[inline] fn set_sr (&self, sr: f64) { self.timebase.set_sr(sr); } @@ -311,7 +303,6 @@ impl Instant { self.set_sample(self.timebase.pulses_to_sample(pulse) as usize); } } - /// (pulses, name), assuming 96 PPQ pub const NOTE_DURATIONS: [(usize, &str);26] = [ (1, "1/384"), @@ -341,7 +332,6 @@ pub const NOTE_DURATIONS: [(usize, &str);26] = [ (3456, "9/1"), (6144, "16/1"), ]; - /// Returns the next shorter length pub fn prev_note_length (pulses: usize) -> usize { for i in 1..=16 { @@ -350,38 +340,20 @@ pub fn prev_note_length (pulses: usize) -> usize { } pulses } - /// Returns the next longer length pub fn next_note_length (pulses: usize) -> usize { for (length, _) in &NOTE_DURATIONS { if *length > pulses { return *length } } pulses } - pub fn pulses_to_name (pulses: usize) -> &'static str { for (length, name) in &NOTE_DURATIONS { if *length == pulses { return name } } "" } - -/// Defines samples per tick. -pub struct Ticks(pub f64); - -impl Ticks { - /// Iterate over ticks between start and end. - pub fn between_samples (&self, start: usize, end: usize) -> TicksIterator { - TicksIterator(self.0, start, start, end) - } -} - -/// Iterator that emits subsequent ticks within a range. -pub struct TicksIterator(f64, usize, usize, usize); - impl Iterator for TicksIterator { type Item = (usize, usize); fn next (&mut self) -> Option { loop { - if self.1 > self.3 { - return None - } + if self.1 > self.3 { return None } let fpt = self.0; let sample = self.1 as f64; let start = self.2; @@ -398,15 +370,12 @@ impl Iterator for TicksIterator { } } } - #[cfg(test)] mod test { use super::*; - #[test] fn test_samples_to_ticks () { let ticks = Ticks(12.3).between_samples(0, 100).collect::>(); println!("{ticks:?}"); } - } diff --git a/crates/tek_sequencer/src/arranger.rs b/crates/tek_sequencer/src/arranger.rs index dd4d78ad..22eac590 100644 --- a/crates/tek_sequencer/src/arranger.rs +++ b/crates/tek_sequencer/src/arranger.rs @@ -73,16 +73,12 @@ pub struct Arrangement { pub struct ArrangementTrack { /// Name of track pub name: Arc>, - /// Inputs - pub inputs: Vec>, - /// MIDI player/recorder - pub player: PhrasePlayer, - /// Outputs - pub outputs: Vec>, /// Preferred width of track column pub width: usize, /// Identifying color of track pub color: Color, + /// MIDI player/recorder + pub player: PhrasePlayer, } #[derive(Default, Debug)] pub struct Scene { @@ -515,11 +511,9 @@ impl ArrangementTrack { pub fn new (clock: &Arc, name: &str, color: Option) -> Self { Self { name: Arc::new(RwLock::new(name.into())), - inputs: vec![], - player: PhrasePlayer::new(clock), - outputs: vec![], width: name.len() + 2, - color: color.unwrap_or_else(random_color) + color: color.unwrap_or_else(random_color), + player: PhrasePlayer::new(clock), } } pub fn longest_name (tracks: &[Self]) -> usize { diff --git a/crates/tek_sequencer/src/arranger_tui.rs b/crates/tek_sequencer/src/arranger_tui.rs index f69e02c2..3e917e1f 100644 --- a/crates/tek_sequencer/src/arranger_tui.rs +++ b/crates/tek_sequencer/src/arranger_tui.rs @@ -171,11 +171,11 @@ impl<'a> Content for VerticalArranger<'a, Tui> { }))?; // track titles let header = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{ - let name = track.name.read().unwrap(); - let max_w = w.saturating_sub(1).min(name.len()).max(2); - let name = format!("▎{}", &name[0..max_w]); - let name = TuiStyle::bold(name, true); - let input_name = track.inputs.get(0) + let name = track.name.read().unwrap(); + let max_w = w.saturating_sub(1).min(name.len()).max(2); + let name = format!("▎{}", &name[0..max_w]); + let name = TuiStyle::bold(name, true); + let input_name = track.player.midi_inputs.get(0) .map(|port|port.short_name()) .transpose()? .unwrap_or("(none)".into()); @@ -190,14 +190,14 @@ impl<'a> Content for VerticalArranger<'a, Tui> { let player = &track.player; let clock = &player.clock; let elapsed = player.phrase.as_ref() - .map(|_|player.frames_since_start()) + .map(|_|player.samples_since_start()) .flatten() .map(|t|format!("▎{t:>}")) .unwrap_or(String::from("▎")); let until_next = player.next_phrase.as_ref() - .map(|(t, _)|format!("▎-{:>}", clock.format_beats(t.load(Ordering::Relaxed)))) + .map(|(t, _)|format!("▎-{:>}", t.format_beat())) .unwrap_or(String::from("▎")); - let output_name = track.outputs.get(0) + let output_name = track.player.midi_outputs.get(0) .map(|port|port.short_name()) .transpose()? .unwrap_or("(none)".into()); @@ -279,11 +279,11 @@ impl<'a> Content for VerticalArranger<'a, Tui> { }, }; if let Some([x, y, width, height]) = track_area { - to.fill_fg([x, y, 1, height], Color::Rgb(70, 80, 50)); + to.fill_fg([x, y, 1, height], Color::Rgb(70, 80, 50)); to.fill_fg([x + width, y, 1, height], Color::Rgb(70, 80, 50)); } if let Some([_, y, _, height]) = scene_area { - to.fill_ul([area.x(), y - 1, area.w(), 1], Color::Rgb(70, 80, 50)); + to.fill_ul([area.x(), y - 1, area.w(), 1], Color::Rgb(70, 80, 50)); to.fill_ul([area.x(), y + height - 1, area.w(), 1], Color::Rgb(70, 80, 50)); } Ok(if focused { @@ -295,7 +295,7 @@ impl<'a> Content for VerticalArranger<'a, Tui> { })) }).bg(bg).grow_y(1).border(border); let color = if self.0.focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)}; - let size = format!("{}x{}", self.0.size.w(), self.0.size.h()); + let size = format!("{}x{}", self.0.size.w(), self.0.size.h()); let lower_right = TuiStyle::fg(size, color).pull_x(1).align_se().fill_xy(); lay!(arrangement, lower_right) } diff --git a/crates/tek_sequencer/src/sequencer.rs b/crates/tek_sequencer/src/sequencer.rs index 988c2432..a34cc209 100644 --- a/crates/tek_sequencer/src/sequencer.rs +++ b/crates/tek_sequencer/src/sequencer.rs @@ -117,31 +117,29 @@ pub struct PhraseEditor { /// Phrase player. pub struct PhrasePlayer { /// Global timebase - pub clock: Arc, + pub clock: Arc, /// Start time and phrase being played - pub phrase: Option<(AtomicUsize, Option>>)>, - /// Start time (FIXME move into phrase, using Instant) - pub started: Option<(usize, usize)>, + pub phrase: Option<(Instant, Option>>)>, /// Start time and next phrase - pub next_phrase: Option<(AtomicUsize, Option>>)>, + pub next_phrase: Option<(Instant, Option>>)>, /// Play input through output. - pub monitoring: bool, + pub monitoring: bool, /// Write input to sequence. - pub recording: bool, + pub recording: bool, /// Overdub input to sequence. - pub overdub: bool, + pub overdub: bool, /// Send all notes off - pub reset: bool, // TODO?: after Some(nframes) + pub reset: bool, // TODO?: after Some(nframes) /// Record from MIDI ports to current sequence. - pub midi_inputs: Vec>, + pub midi_inputs: Vec>, /// Play from current sequence to MIDI ports - pub midi_outputs: Vec>, + pub midi_outputs: Vec>, /// MIDI output buffer - pub midi_out_buf: Vec>>, + pub midi_out_buf: Vec>>, /// Notes currently held at input - pub notes_in: Arc>, + pub notes_in: Arc>, /// Notes currently held at output - pub notes_out: Arc>, + pub notes_out: Arc>, } /// Focus layout of sequencer app impl FocusGrid for Sequencer { @@ -377,7 +375,6 @@ impl PhrasePlayer { Self { clock: clock.clone(), phrase: None, - started: None, next_phrase: None, notes_in: Arc::new(RwLock::new([false;128])), notes_out: Arc::new(RwLock::new([false;128])), @@ -395,21 +392,22 @@ impl PhrasePlayer { pub fn toggle_overdub (&mut self) { self.overdub = !self.overdub; } pub fn enqueue_next (&mut self, phrase: Option<&Arc>>) { let start = self.clock.next_launch_pulse(); - self.next_phrase = Some((start.into(), phrase.map(|p|p.clone()))); + self.next_phrase = Some(( + Instant::from_pulse(&self.clock.timebase, start as f64), + phrase.map(|p|p.clone()) + )); self.reset = true; } - pub fn frames_since_start (&self) -> Option { + pub fn samples_since_start (&self) -> Option { self.phrase.as_ref() - .map(|(started,_)|started.load(Ordering::Relaxed)) + .map(|(started,_)|started.sample()) .map(|started|started - self.clock.sample()) } pub fn playing_phrase (&self) -> Option<(usize, Arc>)> { if let ( - Some(TransportState::Rolling), - Some((start_frame, _)), - Some((_started, Some(ref phrase))) - ) = (*self.clock.playing.read().unwrap(), self.started, &self.phrase) { - Some((start_frame, phrase.clone())) + Some(TransportState::Rolling), Some((started, Some(ref phrase))) + ) = (*self.clock.playing.read().unwrap(), &self.phrase) { + Some((started.sample(), phrase.clone())) } else { None } diff --git a/crates/tek_sequencer/src/sequencer_snd.rs b/crates/tek_sequencer/src/sequencer_snd.rs index 7cccbee2..97e429d8 100644 --- a/crates/tek_sequencer/src/sequencer_snd.rs +++ b/crates/tek_sequencer/src/sequencer_snd.rs @@ -26,16 +26,17 @@ impl Audio for PhrasePlayer { let output = &mut self.midi_out_buf; let notes_on = &mut self.notes_out.write().unwrap(); let frame0 = frame0.saturating_sub(start_frame); + let frameN = frame0 + frames; + let ticks = self.clock.timebase.pulses_between_samples(frame0, frameN); let mut buf = Vec::with_capacity(8); - let ticks = Ticks(self.clock.timebase.pulses_per_sample()); - for (time, tick) in ticks.between_samples(frame0, frame0 + frames) { + for (sample, tick) in ticks { let tick = tick % phrase.length; for message in phrase.notes[tick].iter() { buf.clear(); let channel = 0.into(); let message = *message; LiveEvent::Midi { channel, message }.write(&mut buf).unwrap(); - output[time as usize].push(buf.clone()); + output[sample].push(buf.clone()); update_keys(notes_on, &message); } } diff --git a/crates/tek_sequencer/src/transport.rs b/crates/tek_sequencer/src/transport.rs index 5606db3c..fc82407e 100644 --- a/crates/tek_sequencer/src/transport.rs +++ b/crates/tek_sequencer/src/transport.rs @@ -2,7 +2,7 @@ use crate::*; #[derive(Debug, Default)] pub struct TransportTime { /// Current sample sr, tempo, and PPQ. - pub timebase: Timebase, + pub timebase: Arc, /// Current moment in time pub instant: Instant, /// Playback state @@ -71,7 +71,6 @@ pub struct TransportToolbar { /// Which item of the transport toolbar is focused #[derive(Clone, Copy, PartialEq)] pub enum TransportToolbarFocus { Bpm, Sync, PlayPause, Clock, Quant, } - impl TransportToolbar { pub fn new ( clock: Option<&Arc>, @@ -90,11 +89,11 @@ impl TransportToolbar { None => { let timebase = Timebase::default(); Arc::new(TransportTime { - playing: Some(TransportState::Stopped).into(), - quant: 24.into(), - sync: (timebase.ppq() as usize * 4).into(), - instant: Instant::default(), - timebase, + playing: Some(TransportState::Stopped).into(), + quant: 24.into(), + sync: (timebase.ppq() as usize * 4).into(), + instant: Instant::default(), + timebase: timebase.into(), }) } } diff --git a/crates/tek_sequencer/src/transport_tui.rs b/crates/tek_sequencer/src/transport_tui.rs index 10c71226..77a0772e 100644 --- a/crates/tek_sequencer/src/transport_tui.rs +++ b/crates/tek_sequencer/src/transport_tui.rs @@ -34,7 +34,7 @@ impl Content for TransportToolbar { ).align_w().fill_x(), self.focus.wrap(self.focused, TransportToolbarFocus::Clock, &{ - let time1 = self.clock.format_current_pulse(); + let time1 = self.clock.format_beat(); let time2 = self.clock.format_current_usec(); row!("B" ,time1.as_str(), " T", time2.as_str()).outset_x(1) }).align_e().fill_x(),