diff --git a/crates/tek_core/src/time.rs b/crates/tek_core/src/time.rs index 56635dc8..57ace68d 100644 --- a/crates/tek_core/src/time.rs +++ b/crates/tek_core/src/time.rs @@ -22,20 +22,98 @@ impl TimeFloat for T where T: TimeUnit + From + Into + Copy {} /// MIDI ticks per beat ppq: AtomicF64, } +impl Timebase { + /// Specify sample rate, BPM and PPQ + 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 { fpt: self.samples_per_pulse(), sample: start, start, end } + } +} /// Represents a point in time in all scales #[derive(Debug, Default)] pub struct Instant { pub timebase: Arc, /// Current time in microseconds - usec: AtomicUsize, + usec: AtomicUsize, /// Current time in audio samples - sample: AtomicUsize, + sample: AtomicUsize, /// Current time in MIDI pulses - pulse: AtomicF64, + pulse: AtomicF64, +} +impl Instant { + pub fn from_usec (timebase: &Arc, usec: usize) -> Self { + Self { + usec: usec.into(), + sample: (timebase.usecs_to_sample(usec as f64) as usize).into(), + pulse: timebase.usecs_to_pulse(usec as f64).into(), + timebase: timebase.clone(), + } + } + pub fn from_sample (timebase: &Arc, sample: usize) -> Self { + Self { + sample: sample.into(), + usec: (timebase.samples_to_usec(sample as f64) as usize).into(), + pulse: timebase.samples_to_pulse(sample as f64).into(), + timebase: timebase.clone(), + } + } + pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { + Self { + pulse: pulse.into(), + sample: (timebase.pulses_to_sample(pulse) as usize).into(), + usec: (timebase.pulses_to_usec(pulse) as usize).into(), + timebase: timebase.clone(), + } + } + pub fn update_from_usec (&self, usec: usize) { + self.set_usec(usec); + self.set_pulse(self.timebase.usecs_to_pulse(usec as f64)); + self.set_sample(self.timebase.usecs_to_sample(usec as f64) as usize); + } + pub fn update_from_sample (&self, sample: usize) { + self.set_usec(self.timebase.samples_to_usec(sample as f64) as usize); + self.set_pulse(self.timebase.samples_to_pulse(sample as f64)); + self.set_sample(sample); + } + pub fn update_from_pulse (&self, pulse: f64) { + self.set_usec(self.timebase.pulses_to_usec(pulse) as usize); + self.set_pulse(pulse); + self.set_sample(self.timebase.pulses_to_sample(pulse) as usize); + } + pub fn format_beat (&self) -> String { + self.format_beats_float(self.pulse()) + } } -/// Defines samples per tick. -pub struct Ticks(pub f64); /// Iterator that emits subsequent ticks within a range. -pub struct TicksIterator(f64, usize, usize, usize); +pub struct TicksIterator { + fpt: f64, + sample: usize, + start: usize, + end: usize, +} +impl Iterator for TicksIterator { + type Item = (usize, usize); + fn next (&mut self) -> Option { + loop { + if self.sample > self.end { return None } + let fpt = self.fpt; + let sample = self.sample as f64; + let start = self.start; + let end = self.end; + self.sample += 1; + //println!("{fpt} {sample} {start} {end}"); + let jitter = sample.rem_euclid(fpt); // ramps + let next_jitter = (sample + 1.0).rem_euclid(fpt); + if jitter > next_jitter { // at crossing: + let time = (sample as usize) % (end as usize-start as usize); + let tick = (sample / fpt) as usize; + return Some((time, tick)) + } + } + } +} /// Trait for struct that defines a sample rate in hertz (samples per second) pub trait SampleRate { /// Get the sample rate @@ -59,6 +137,14 @@ pub trait SampleRate { usecs * self.sample_per_usec() } } +impl SampleRate for Timebase { + #[inline] fn sr (&self) -> f64 { self.sr.load(Ordering::Relaxed) } + #[inline] fn set_sr (&self, sr: f64) { self.sr.store(sr, Ordering::Relaxed); } +} +impl SampleRate for Instant { + #[inline] fn sr (&self) -> f64 { self.timebase.sr() } + #[inline] fn set_sr (&self, sr: f64) { self.timebase.set_sr(sr); } +} /// Trait for struct that defines a tempo in beats per minute pub trait BeatsPerMinute { /// Get the tempo @@ -97,6 +183,14 @@ pub trait BeatsPerMinute { events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() } } +impl BeatsPerMinute for Timebase { + #[inline] fn bpm (&self) -> f64 { self.bpm.load(Ordering::Relaxed) } + #[inline] fn set_bpm (&self, bpm: f64) { self.bpm.store(bpm, Ordering::Relaxed); } +} +impl BeatsPerMinute for Instant { + #[inline] fn bpm (&self) -> f64 { self.timebase.bpm() } + #[inline] fn set_bpm (&self, bpm: f64) { self.timebase.set_bpm(bpm); } +} /// Trait for struct that defines a MIDI resolution in pulses per quaver (beat) pub trait PulsesPerQuaver { const DEFAULT_PPQ: U; @@ -158,7 +252,7 @@ pub trait PulsesPerQuaver { { self.sr() / self.pulses_per_second() } - #[inline] fn format_beats (&self, pulse: U) -> String where U: TimeInteger { + #[inline] fn format_beats_int (&self, pulse: U) -> String where U: TimeInteger { let ppq = self.ppq(); let (beats, pulses) = if ppq > U::from(0) { (pulse / ppq, pulse % ppq) @@ -169,19 +263,47 @@ pub trait PulsesPerQuaver { let beats = (beats % U::from(4)) + U::from(1); format!("{bars}.{beats}.{pulses:02}") } + #[inline] fn format_beats_float (&self, pulse: U) -> String where U: TimeFloat { + let ppq = self.ppq(); + let (beats, pulses) = if ppq > U::from(0.) { + (pulse / ppq, pulse % ppq) + } else { + (U::from(0.), U::from(0.)) + }; + let bars = (beats / U::from(4.)) + U::from(1.); + let beats = (beats % U::from(4.)) + U::from(1.); + format!("{bars}.{beats}.{pulses:02}") + } +} +impl PulsesPerQuaver for Timebase { + const DEFAULT_PPQ: f64 = 96f64; + #[inline] fn ppq (&self) -> f64 { self.ppq.load(Ordering::Relaxed) } + #[inline] fn set_ppq (&self, ppq: f64) { self.ppq.store(ppq, Ordering::Relaxed); } +} +impl PulsesPerQuaver for Instant { + const DEFAULT_PPQ: f64 = 96f64; + #[inline] fn ppq (&self) -> f64 { self.timebase.ppq() } + #[inline] fn set_ppq (&self, ppq: f64) { self.timebase.set_ppq(ppq); } } pub trait SamplePosition { fn sample (&self) -> U; fn set_sample (&self, sample: U); } +impl SamplePosition for Instant { + #[inline] fn sample (&self) -> usize { self.sample.load(Ordering::Relaxed) } + #[inline] fn set_sample (&self, s: usize) { self.sample.store(s, Ordering::Relaxed); } +} pub trait PulsePosition { fn pulse (&self) -> U; fn set_pulse (&self, pulse: U); - #[inline] fn format_beat (&self) -> String - where Self: PulsesPerQuaver, U: TimeInteger - { - self.format_beats(self.pulse()) - } +} +impl PulsePosition for Instant { + #[inline] fn pulse (&self) -> f64 { self.pulse.load(Ordering::Relaxed) } + #[inline] fn set_pulse (&self, p: f64) { self.pulse.store(p, Ordering::Relaxed); } +} +impl PulsePosition for Instant { + #[inline] fn pulse (&self) -> usize { self.pulse.load(Ordering::Relaxed) as usize } + #[inline] fn set_pulse (&self, p: usize) { PulsePosition::::set_pulse(self, p as f64) } } pub trait UsecPosition { fn usec (&self) -> U; @@ -193,6 +315,10 @@ pub trait UsecPosition { format!("{minutes}:{seconds:02}:{msecs:03}") } } +impl UsecPosition for Instant { + #[inline] fn usec (&self) -> usize { self.usec.load(Ordering::Relaxed) } + #[inline] fn set_usec (&self, u: usize) { self.usec.store(u, Ordering::Relaxed); } +} pub trait LaunchSync { fn sync (&self) -> U; fn set_sync (&self, sync: U); @@ -211,98 +337,6 @@ pub trait Quantize { fn set_quant (&self, quant: T); } 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) } - #[inline] fn set_sr (&self, sr: f64) { self.sr.store(sr, Ordering::Relaxed); } -} -impl BeatsPerMinute for Timebase { - #[inline] fn bpm (&self) -> f64 { self.bpm.load(Ordering::Relaxed) } - #[inline] fn set_bpm (&self, bpm: f64) { self.bpm.store(bpm, Ordering::Relaxed); } -} -impl PulsesPerQuaver for Timebase { - const DEFAULT_PPQ: f64 = 96f64; - #[inline] fn ppq (&self) -> f64 { self.ppq.load(Ordering::Relaxed) } - #[inline] fn set_ppq (&self, ppq: f64) { self.ppq.store(ppq, Ordering::Relaxed); } -} -impl SampleRate for Instant { - #[inline] fn sr (&self) -> f64 { self.timebase.sr() } - #[inline] fn set_sr (&self, sr: f64) { self.timebase.set_sr(sr); } -} -impl BeatsPerMinute for Instant { - #[inline] fn bpm (&self) -> f64 { self.timebase.bpm() } - #[inline] fn set_bpm (&self, bpm: f64) { self.timebase.set_bpm(bpm); } -} -impl PulsesPerQuaver for Instant { - const DEFAULT_PPQ: f64 = 96f64; - #[inline] fn ppq (&self) -> f64 { self.timebase.ppq() } - #[inline] fn set_ppq (&self, ppq: f64) { self.timebase.set_ppq(ppq); } -} -impl SamplePosition for Instant { - #[inline] fn sample (&self) -> usize { self.sample.load(Ordering::Relaxed) } - #[inline] fn set_sample (&self, s: usize) { self.sample.store(s, Ordering::Relaxed); } -} -impl UsecPosition for Instant { - #[inline] fn usec (&self) -> usize { self.usec.load(Ordering::Relaxed) } - #[inline] fn set_usec (&self, u: usize) { self.usec.store(u, Ordering::Relaxed); } -} -impl PulsePosition for Instant { - #[inline] fn pulse (&self) -> f64 { self.pulse.load(Ordering::Relaxed) } - #[inline] fn set_pulse (&self, p: f64) { self.pulse.store(p, Ordering::Relaxed); } -} -impl PulsePosition for Instant { - #[inline] fn pulse (&self) -> usize { self.pulse.load(Ordering::Relaxed) as usize } - #[inline] fn set_pulse (&self, p: usize) { PulsePosition::::set_pulse(self, p as f64) } -} -impl Instant { - pub fn from_usec (timebase: &Arc, usec: usize) -> Self { - Self { - usec: usec.into(), - sample: (timebase.usecs_to_sample(usec as f64) as usize).into(), - pulse: timebase.usecs_to_pulse(usec as f64).into(), - timebase: timebase.clone(), - } - } - pub fn update_from_usec (&self, usec: usize) { - self.set_usec(usec); - self.set_pulse(self.timebase.usecs_to_pulse(usec as f64)); - self.set_sample(self.timebase.usecs_to_sample(usec as f64) as usize); - } - pub fn from_sample (timebase: &Arc, sample: usize) -> Self { - Self { - sample: sample.into(), - usec: (timebase.samples_to_usec(sample as f64) as usize).into(), - pulse: timebase.samples_to_pulse(sample as f64).into(), - timebase: timebase.clone(), - } - } - pub fn update_from_sample (&self, sample: usize) { - self.set_sample(sample); - self.set_usec(self.timebase.samples_to_usec(sample as f64) as usize); - self.set_pulse(self.timebase.samples_to_pulse(sample as f64)); - } - pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { - Self { - pulse: pulse.into(), - sample: (timebase.pulses_to_sample(pulse) as usize).into(), - usec: (timebase.pulses_to_usec(pulse) as usize).into(), - timebase: timebase.clone(), - } - } - pub fn update_from_pulse (&self, pulse: f64) { - self.set_pulse(pulse); - self.set_usec(self.timebase.pulses_to_usec(pulse) as usize); - 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"), @@ -349,27 +383,6 @@ pub fn pulses_to_name (pulses: usize) -> &'static str { for (length, name) in &NOTE_DURATIONS { if *length == pulses { return name } } "" } -impl Iterator for TicksIterator { - type Item = (usize, usize); - fn next (&mut self) -> Option { - loop { - if self.1 > self.3 { return None } - let fpt = self.0; - let sample = self.1 as f64; - let start = self.2; - let end = self.3; - self.1 = self.1 + 1; - //println!("{fpt} {sample} {start} {end}"); - let jitter = sample.rem_euclid(fpt); // ramps - let next_jitter = (sample + 1.0).rem_euclid(fpt); - if jitter > next_jitter { // at crossing: - let time = (sample as usize) % (end as usize-start as usize); - let tick = (sample / fpt) as usize; - return Some((time, tick)) - } - } - } -} #[cfg(test)] mod test { use super::*; diff --git a/crates/tek_sequencer/src/arranger_tui.rs b/crates/tek_sequencer/src/arranger_tui.rs index 3e917e1f..3d206d47 100644 --- a/crates/tek_sequencer/src/arranger_tui.rs +++ b/crates/tek_sequencer/src/arranger_tui.rs @@ -179,7 +179,7 @@ impl<'a> Content for VerticalArranger<'a, Tui> { .map(|port|port.short_name()) .transpose()? .unwrap_or("(none)".into()); - let input = format!("▎>{}", input_name); + let input = format!("▎>{}", input_name); col!(name, input) .min_xy(w as u16, track_title_h) .bg(track.color) diff --git a/crates/tek_sequencer/src/sequencer.rs b/crates/tek_sequencer/src/sequencer.rs index a34cc209..a23f80d6 100644 --- a/crates/tek_sequencer/src/sequencer.rs +++ b/crates/tek_sequencer/src/sequencer.rs @@ -401,7 +401,7 @@ impl PhrasePlayer { pub fn samples_since_start (&self) -> Option { self.phrase.as_ref() .map(|(started,_)|started.sample()) - .map(|started|started - self.clock.sample()) + .map(|started|started - self.clock.instant.sample()) } pub fn playing_phrase (&self) -> Option<(usize, Arc>)> { if let ( diff --git a/crates/tek_sequencer/src/transport.rs b/crates/tek_sequencer/src/transport.rs index fc82407e..0d661134 100644 --- a/crates/tek_sequencer/src/transport.rs +++ b/crates/tek_sequencer/src/transport.rs @@ -12,32 +12,6 @@ pub struct TransportTime { /// Launch quantization factor pub sync: AtomicUsize, } -impl SampleRate for TransportTime { - #[inline] fn sr (&self) -> f64 { self.timebase.sr() } - #[inline] fn set_sr (&self, sr: f64) { self.timebase.set_sr(sr); } -} -impl BeatsPerMinute for TransportTime { - #[inline] fn bpm (&self) -> f64 { self.timebase.bpm() } - #[inline] fn set_bpm (&self, bpm: f64) { self.timebase.set_bpm(bpm); } -} -impl PulsesPerQuaver for TransportTime { - const DEFAULT_PPQ: f64 = Timebase::DEFAULT_PPQ; - #[inline] fn ppq (&self) -> f64 { self.timebase.ppq() } - #[inline] fn set_ppq (&self, ppq: f64) { self.timebase.set_ppq(ppq); } -} -impl PulsesPerQuaver for TransportTime { - const DEFAULT_PPQ: usize = Timebase::DEFAULT_PPQ as usize; - #[inline] fn ppq (&self) -> usize { self.timebase.ppq() as usize } - #[inline] fn set_ppq (&self, ppq: usize) { self.timebase.set_ppq(ppq as f64); } -} -impl SamplePosition for TransportTime { - #[inline] fn sample (&self) -> usize { self.instant.sample() } - #[inline] fn set_sample (&self, sample: usize) { self.instant.set_sample(sample) } -} -impl UsecPosition for TransportTime { - #[inline] fn usec (&self) -> usize { self.instant.usec() } - #[inline] fn set_usec (&self, usec: usize) { self.instant.set_usec(usec) } -} impl PulsePosition for TransportTime { #[inline] fn pulse (&self) -> usize { self.instant.pulse() } #[inline] fn set_pulse (&self, usec: usize) { self.instant.set_pulse(usec); } diff --git a/crates/tek_sequencer/src/transport_cmd.rs b/crates/tek_sequencer/src/transport_cmd.rs index 0ef3058f..aade9a60 100644 --- a/crates/tek_sequencer/src/transport_cmd.rs +++ b/crates/tek_sequencer/src/transport_cmd.rs @@ -24,11 +24,12 @@ impl TransportToolbar { Ok(Some(true)) } fn handle_bpm (&mut self, from: &TuiInput) -> Perhaps { + let bpm = self.clock.timebase.bpm(); match from.event() { - key!(KeyCode::Char(',')) => { self.clock.set_bpm(self.clock.bpm() - 1.0); }, - key!(KeyCode::Char('.')) => { self.clock.set_bpm(self.clock.bpm() + 1.0); }, - key!(KeyCode::Char('<')) => { self.clock.set_bpm(self.clock.bpm() - 0.001); }, - key!(KeyCode::Char('>')) => { self.clock.set_bpm(self.clock.bpm() + 0.001); }, + key!(KeyCode::Char(',')) => { self.clock.timebase.set_bpm(bpm - 1.0); }, + key!(KeyCode::Char('.')) => { self.clock.timebase.set_bpm(bpm + 1.0); }, + key!(KeyCode::Char('<')) => { self.clock.timebase.set_bpm(bpm - 0.001); }, + key!(KeyCode::Char('>')) => { self.clock.timebase.set_bpm(bpm + 0.001); }, _ => return Ok(None) } Ok(Some(true)) diff --git a/crates/tek_sequencer/src/transport_snd.rs b/crates/tek_sequencer/src/transport_snd.rs index aaa7a916..8bfea929 100644 --- a/crates/tek_sequencer/src/transport_snd.rs +++ b/crates/tek_sequencer/src/transport_snd.rs @@ -5,7 +5,7 @@ impl Audio for TransportToolbar { let CycleTimes { current_frames, current_usecs, next_usecs: _, period_usecs: _ } = times; let _chunk_size = scope.n_frames() as usize; let transport = self.transport.as_ref().unwrap().query().unwrap(); - self.clock.set_sample(transport.pos.frame() as usize); + self.clock.instant.set_sample(transport.pos.frame() as usize); if *self.clock.playing.read().unwrap() != Some(transport.state) { self.started = match transport.state { TransportState::Rolling => Some((current_frames as usize, current_usecs as usize)), diff --git a/crates/tek_sequencer/src/transport_tui.rs b/crates/tek_sequencer/src/transport_tui.rs index 77a0772e..13e416aa 100644 --- a/crates/tek_sequencer/src/transport_tui.rs +++ b/crates/tek_sequencer/src/transport_tui.rs @@ -19,11 +19,9 @@ impl Content for TransportToolbar { ).min_xy(11, 2).push_x(1)).align_x().fill_x(), row!( - self.focus.wrap(self.focused, TransportToolbarFocus::Bpm, &Outset::X(1u16, row! { - "BPM ", format!("{}.{:03}", - self.clock.bpm() as usize, - (self.clock.bpm() * 1000.0) % 1000.0 - ) + self.focus.wrap(self.focused, TransportToolbarFocus::Bpm, &Outset::X(1u16, { + let bpm = self.clock.timebase.bpm(); + row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) } })), //let quant = self.focus.wrap(self.focused, TransportToolbarFocus::Quant, &Outset::X(1u16, row! { //"QUANT ", ppq_to_name(self.quant as usize) @@ -34,8 +32,8 @@ impl Content for TransportToolbar { ).align_w().fill_x(), self.focus.wrap(self.focused, TransportToolbarFocus::Clock, &{ - let time1 = self.clock.format_beat(); - let time2 = self.clock.format_current_usec(); + let time1 = self.clock.instant.format_beat(); + let time2 = self.clock.instant.format_current_usec(); row!("B" ,time1.as_str(), " T", time2.as_str()).outset_x(1) }).align_e().fill_x(),