simplify time traits

This commit is contained in:
🪞👃🪞 2024-11-01 13:31:27 +02:00
parent ad2f75bee6
commit 66f9afe500
7 changed files with 151 additions and 165 deletions

View file

@ -22,20 +22,98 @@ impl<T> TimeFloat for T where T: TimeUnit + From<f64> + Into<f64> + Copy {}
/// MIDI ticks per beat
ppq: AtomicF64,
}
impl Timebase {
/// Specify sample rate, BPM and PPQ
pub fn new (s: impl Into<AtomicF64>, b: impl Into<AtomicF64>, p: impl Into<AtomicF64>) -> 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<Timebase>,
/// 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<Timebase>, 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<Timebase>, 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<Timebase>, 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<Self::Item> {
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<U: TimeUnit> {
/// Get the sample rate
@ -59,6 +137,14 @@ pub trait SampleRate<U: TimeUnit> {
usecs * self.sample_per_usec()
}
}
impl SampleRate<f64> 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<f64> 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<U: TimeFloat> {
/// Get the tempo
@ -97,6 +183,14 @@ pub trait BeatsPerMinute<U: TimeFloat> {
events.map(|(time, event)|(self.quantize(step, time).0, event)).collect()
}
}
impl BeatsPerMinute<f64> 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<f64> 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<U: TimeUnit> {
const DEFAULT_PPQ: U;
@ -158,7 +252,7 @@ pub trait PulsesPerQuaver<U: TimeUnit> {
{
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<U: TimeUnit> {
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<f64> 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<f64> 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<U: TimeUnit> {
fn sample (&self) -> U;
fn set_sample (&self, sample: U);
}
impl SamplePosition<usize> 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<U: TimeUnit> {
fn pulse (&self) -> U;
fn set_pulse (&self, pulse: U);
#[inline] fn format_beat (&self) -> String
where Self: PulsesPerQuaver<U>, U: TimeInteger
{
self.format_beats(self.pulse())
}
}
impl PulsePosition<f64> 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<usize> for Instant {
#[inline] fn pulse (&self) -> usize { self.pulse.load(Ordering::Relaxed) as usize }
#[inline] fn set_pulse (&self, p: usize) { PulsePosition::<f64>::set_pulse(self, p as f64) }
}
pub trait UsecPosition<U: TimeUnit> {
fn usec (&self) -> U;
@ -193,6 +315,10 @@ pub trait UsecPosition<U: TimeUnit> {
format!("{minutes}:{seconds:02}:{msecs:03}")
}
}
impl UsecPosition<usize> 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<U: TimeUnit> {
fn sync (&self) -> U;
fn set_sync (&self, sync: U);
@ -211,98 +337,6 @@ pub trait Quantize<T> {
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<AtomicF64>, b: impl Into<AtomicF64>, p: impl Into<AtomicF64>) -> 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<f64> 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<f64> 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<f64> 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<f64> for Instant {
#[inline] fn sr (&self) -> f64 { self.timebase.sr() }
#[inline] fn set_sr (&self, sr: f64) { self.timebase.set_sr(sr); }
}
impl BeatsPerMinute<f64> for Instant {
#[inline] fn bpm (&self) -> f64 { self.timebase.bpm() }
#[inline] fn set_bpm (&self, bpm: f64) { self.timebase.set_bpm(bpm); }
}
impl PulsesPerQuaver<f64> 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<usize> 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<usize> 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<f64> 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<usize> for Instant {
#[inline] fn pulse (&self) -> usize { self.pulse.load(Ordering::Relaxed) as usize }
#[inline] fn set_pulse (&self, p: usize) { PulsePosition::<f64>::set_pulse(self, p as f64) }
}
impl Instant {
pub fn from_usec (timebase: &Arc<Timebase>, 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<Timebase>, 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<Timebase>, 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<Self::Item> {
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::*;

View file

@ -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)

View file

@ -401,7 +401,7 @@ impl PhrasePlayer {
pub fn samples_since_start (&self) -> Option<usize> {
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<RwLock<Phrase>>)> {
if let (

View file

@ -12,32 +12,6 @@ pub struct TransportTime {
/// Launch quantization factor
pub sync: AtomicUsize,
}
impl SampleRate<f64> for TransportTime {
#[inline] fn sr (&self) -> f64 { self.timebase.sr() }
#[inline] fn set_sr (&self, sr: f64) { self.timebase.set_sr(sr); }
}
impl BeatsPerMinute<f64> for TransportTime {
#[inline] fn bpm (&self) -> f64 { self.timebase.bpm() }
#[inline] fn set_bpm (&self, bpm: f64) { self.timebase.set_bpm(bpm); }
}
impl PulsesPerQuaver<f64> 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<usize> 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<usize> for TransportTime {
#[inline] fn sample (&self) -> usize { self.instant.sample() }
#[inline] fn set_sample (&self, sample: usize) { self.instant.set_sample(sample) }
}
impl UsecPosition<usize> for TransportTime {
#[inline] fn usec (&self) -> usize { self.instant.usec() }
#[inline] fn set_usec (&self, usec: usize) { self.instant.set_usec(usec) }
}
impl PulsePosition<usize> for TransportTime {
#[inline] fn pulse (&self) -> usize { self.instant.pulse() }
#[inline] fn set_pulse (&self, usec: usize) { self.instant.set_pulse(usec); }

View file

@ -24,11 +24,12 @@ impl TransportToolbar<Tui> {
Ok(Some(true))
}
fn handle_bpm (&mut self, from: &TuiInput) -> Perhaps<bool> {
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))

View file

@ -5,7 +5,7 @@ impl<E: Engine> Audio for TransportToolbar<E> {
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)),

View file

@ -19,11 +19,9 @@ impl Content for TransportToolbar<Tui> {
).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<Tui> {
).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(),