From f1a8d9e84686ff8dbf2e24f67246f84242a62939 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 16:07:46 +0100 Subject: [PATCH] refactor core::space --- crates/tek/src/core/engine.rs | 5 - crates/tek/src/space.rs | 165 +------------- crates/tek/src/space/area.rs | 73 ++++++ crates/tek/src/space/coord.rs | 37 ++++ crates/tek/src/space/direction.rs | 28 +++ crates/tek/src/space/size.rs | 26 +++ crates/tek/src/time.rs | 357 +----------------------------- crates/tek/src/time/bpm.rs | 5 + crates/tek/src/time/moment.rs | 69 ++++++ crates/tek/src/time/ppq.rs | 42 ++++ crates/tek/src/time/quant.rs | 25 +++ crates/tek/src/time/sr.rs | 27 +++ crates/tek/src/time/timebase.rs | 112 ++++++++++ crates/tek/src/time/unit.rs | 61 +++++ crates/tek/src/time/usec.rs | 13 ++ 15 files changed, 532 insertions(+), 513 deletions(-) create mode 100644 crates/tek/src/space/area.rs create mode 100644 crates/tek/src/space/coord.rs create mode 100644 crates/tek/src/space/direction.rs create mode 100644 crates/tek/src/space/size.rs create mode 100644 crates/tek/src/time/bpm.rs create mode 100644 crates/tek/src/time/moment.rs create mode 100644 crates/tek/src/time/ppq.rs create mode 100644 crates/tek/src/time/quant.rs create mode 100644 crates/tek/src/time/sr.rs create mode 100644 crates/tek/src/time/timebase.rs create mode 100644 crates/tek/src/time/unit.rs create mode 100644 crates/tek/src/time/usec.rs diff --git a/crates/tek/src/core/engine.rs b/crates/tek/src/core/engine.rs index 10effc1b..fb9ef473 100644 --- a/crates/tek/src/core/engine.rs +++ b/crates/tek/src/core/engine.rs @@ -1,10 +1,5 @@ use crate::*; -/// Entry point for main loop -pub trait App { - fn run (self, context: T) -> Usually; -} - /// Platform backend. pub trait Engine: Send + Sync + Sized { /// Input event type diff --git a/crates/tek/src/space.rs b/crates/tek/src/space.rs index 490dcc60..bb2ca859 100644 --- a/crates/tek/src/space.rs +++ b/crates/tek/src/space.rs @@ -1,164 +1,7 @@ -use crate::*; - -/// Standard numeric type. -pub trait Coordinate: Send + Sync + Copy - + Add - + Sub - + Mul - + Div - + Ord + PartialEq + Eq - + Debug + Display + Default - + From + Into - + Into - + Into -{ - fn minus (self, other: Self) -> Self { - if self >= other { - self - other - } else { - 0.into() - } - } - fn ZERO () -> Self { - 0.into() - } -} - -impl Coordinate for T where T: Send + Sync + Copy - + Add - + Sub - + Mul - + Div - + Ord + PartialEq + Eq - + Debug + Display + Default - + From + Into - + Into - + Into -{} +pub(crate) mod coord; pub(crate) use coord::*; +pub(crate) mod size; pub(crate) use size::*; +pub(crate) mod area; pub(crate) use area::*; +pub(crate) mod direction; pub(crate) use direction::*; // TODO: return impl Point and impl Size instead of [N;x] // to disambiguate between usage of 2-"tuple"s - -pub trait Size { - fn x (&self) -> N; - fn y (&self) -> N; - #[inline] fn w (&self) -> N { self.x() } - #[inline] fn h (&self) -> N { self.y() } - #[inline] fn wh (&self) -> [N;2] { [self.x(), self.y()] } - #[inline] fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w.into()), self.h()] } - #[inline] fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h.into())] } - #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { - if self.w() < w || self.h() < h { - Err(format!("min {w}x{h}").into()) - } else { - Ok(self) - } - } -} -impl Size for (N, N) { - fn x (&self) -> N { self.0 } - fn y (&self) -> N { self.1 } -} -impl Size for [N;2] { - fn x (&self) -> N { self[0] } - fn y (&self) -> N { self[1] } -} - -pub trait Area: Copy { - fn x (&self) -> N; - fn y (&self) -> N; - fn w (&self) -> N; - fn h (&self) -> N; - fn x2 (&self) -> N { self.x() + self.w() } - fn y2 (&self) -> N { self.y() + self.h() } - #[inline] fn wh (&self) -> [N;2] { [self.w(), self.h()] } - #[inline] fn xywh (&self) -> [N;4] { [self.x(), self.y(), self.w(), self.h()] } - #[inline] fn lrtb (&self) -> [N;4] { [self.x(), self.x2(), self.y(), self.y2()] } - #[inline] fn push_x (&self, x: N) -> [N;4] { [self.x() + x, self.y(), self.w(), self.h()] } - #[inline] fn push_y (&self, y: N) -> [N;4] { [self.x(), self.y() + y, self.w(), self.h()] } - #[inline] fn shrink_x (&self, x: N) -> [N;4] { - [self.x(), self.y(), self.w().minus(x), self.h()] - } - #[inline] fn shrink_y (&self, y: N) -> [N;4] { - [self.x(), self.y(), self.w(), self.h().minus(y)] - } - #[inline] fn set_w (&self, w: N) -> [N;4] { [self.x(), self.y(), w, self.h()] } - #[inline] fn set_h (&self, h: N) -> [N;4] { [self.x(), self.y(), self.w(), h] } - #[inline] fn clip_h (&self, h: N) -> [N;4] { - [self.x(), self.y(), self.w(), self.h().min(h.into())] - } - #[inline] fn clip_w (&self, w: N) -> [N;4] { - [self.x(), self.y(), self.w().min(w.into()), self.h()] - } - #[inline] fn clip (&self, wh: impl Size) -> [N;4] { - [self.x(), self.y(), wh.w(), wh.h()] - } - #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { - if self.w() < w || self.h() < h { - Err(format!("min {w}x{h}").into()) - } else { - Ok(self) - } - } - #[inline] fn split_fixed (&self, direction: Direction, a: N) -> ([N;4],[N;4]) { - match direction { - Direction::Up => ( - [self.x(), (self.y()+self.h()).minus(a), self.w(), a], - [self.x(), self.y(), self.w(), self.h().minus(a)], - ), - Direction::Down => ( - [self.x(), self.y(), self.w(), a], - [self.x(), self.y() + a, self.w(), self.h().minus(a)], - ), - Direction::Right => ( - [self.x(), self.y(), a, self.h()], - [self.x() + a, self.y(), self.w().minus(a), self.h()], - ), - Direction::Left => ( - [self.x() + self.w() - a, self.y(), a, self.h()], - [self.x(), self.y(), self.w() - a, self.h()], - ), - } - } -} - -impl Area for (N, N, N, N) { - #[inline] fn x (&self) -> N { self.0 } - #[inline] fn y (&self) -> N { self.1 } - #[inline] fn w (&self) -> N { self.2 } - #[inline] fn h (&self) -> N { self.3 } -} - -impl Area for [N;4] { - #[inline] fn x (&self) -> N { self[0] } - #[inline] fn y (&self) -> N { self[1] } - #[inline] fn w (&self) -> N { self[2] } - #[inline] fn h (&self) -> N { self[3] } -} - -#[derive(Copy, Clone, PartialEq)] -pub enum Direction { Up, Down, Left, Right, } -impl Direction { - pub fn is_up (&self) -> bool { match self { Self::Up => true, _ => false } } - pub fn is_down (&self) -> bool { match self { Self::Down => true, _ => false } } - pub fn is_left (&self) -> bool { match self { Self::Left => true, _ => false } } - pub fn is_right (&self) -> bool { match self { Self::Right => true, _ => false } } - /// Return next direction clockwise - pub fn cw (&self) -> Self { - match self { - Self::Up => Self::Right, - Self::Down => Self::Left, - Self::Left => Self::Up, - Self::Right => Self::Down, - } - } - /// Return next direction counterclockwise - pub fn ccw (&self) -> Self { - match self { - Self::Up => Self::Left, - Self::Down => Self::Right, - Self::Left => Self::Down, - Self::Right => Self::Up, - } - } -} diff --git a/crates/tek/src/space/area.rs b/crates/tek/src/space/area.rs new file mode 100644 index 00000000..c1c8d350 --- /dev/null +++ b/crates/tek/src/space/area.rs @@ -0,0 +1,73 @@ +use crate::*; + +pub trait Area: Copy { + fn x (&self) -> N; + fn y (&self) -> N; + fn w (&self) -> N; + fn h (&self) -> N; + fn x2 (&self) -> N { self.x() + self.w() } + fn y2 (&self) -> N { self.y() + self.h() } + #[inline] fn wh (&self) -> [N;2] { [self.w(), self.h()] } + #[inline] fn xywh (&self) -> [N;4] { [self.x(), self.y(), self.w(), self.h()] } + #[inline] fn lrtb (&self) -> [N;4] { [self.x(), self.x2(), self.y(), self.y2()] } + #[inline] fn push_x (&self, x: N) -> [N;4] { [self.x() + x, self.y(), self.w(), self.h()] } + #[inline] fn push_y (&self, y: N) -> [N;4] { [self.x(), self.y() + y, self.w(), self.h()] } + #[inline] fn shrink_x (&self, x: N) -> [N;4] { + [self.x(), self.y(), self.w().minus(x), self.h()] + } + #[inline] fn shrink_y (&self, y: N) -> [N;4] { + [self.x(), self.y(), self.w(), self.h().minus(y)] + } + #[inline] fn set_w (&self, w: N) -> [N;4] { [self.x(), self.y(), w, self.h()] } + #[inline] fn set_h (&self, h: N) -> [N;4] { [self.x(), self.y(), self.w(), h] } + #[inline] fn clip_h (&self, h: N) -> [N;4] { + [self.x(), self.y(), self.w(), self.h().min(h.into())] + } + #[inline] fn clip_w (&self, w: N) -> [N;4] { + [self.x(), self.y(), self.w().min(w.into()), self.h()] + } + #[inline] fn clip (&self, wh: impl Size) -> [N;4] { + [self.x(), self.y(), wh.w(), wh.h()] + } + #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { + if self.w() < w || self.h() < h { + Err(format!("min {w}x{h}").into()) + } else { + Ok(self) + } + } + #[inline] fn split_fixed (&self, direction: Direction, a: N) -> ([N;4],[N;4]) { + match direction { + Direction::Up => ( + [self.x(), (self.y()+self.h()).minus(a), self.w(), a], + [self.x(), self.y(), self.w(), self.h().minus(a)], + ), + Direction::Down => ( + [self.x(), self.y(), self.w(), a], + [self.x(), self.y() + a, self.w(), self.h().minus(a)], + ), + Direction::Right => ( + [self.x(), self.y(), a, self.h()], + [self.x() + a, self.y(), self.w().minus(a), self.h()], + ), + Direction::Left => ( + [self.x() + self.w() - a, self.y(), a, self.h()], + [self.x(), self.y(), self.w() - a, self.h()], + ), + } + } +} + +impl Area for (N, N, N, N) { + #[inline] fn x (&self) -> N { self.0 } + #[inline] fn y (&self) -> N { self.1 } + #[inline] fn w (&self) -> N { self.2 } + #[inline] fn h (&self) -> N { self.3 } +} + +impl Area for [N;4] { + #[inline] fn x (&self) -> N { self[0] } + #[inline] fn y (&self) -> N { self[1] } + #[inline] fn w (&self) -> N { self[2] } + #[inline] fn h (&self) -> N { self[3] } +} diff --git a/crates/tek/src/space/coord.rs b/crates/tek/src/space/coord.rs new file mode 100644 index 00000000..7feac7dc --- /dev/null +++ b/crates/tek/src/space/coord.rs @@ -0,0 +1,37 @@ +use crate::*; + +/// Standard numeric type. +pub trait Coordinate: Send + Sync + Copy + + Add + + Sub + + Mul + + Div + + Ord + PartialEq + Eq + + Debug + Display + Default + + From + Into + + Into + + Into +{ + fn minus (self, other: Self) -> Self { + if self >= other { + self - other + } else { + 0.into() + } + } + fn ZERO () -> Self { + 0.into() + } +} + +impl Coordinate for T where T: Send + Sync + Copy + + Add + + Sub + + Mul + + Div + + Ord + PartialEq + Eq + + Debug + Display + Default + + From + Into + + Into + + Into +{} diff --git a/crates/tek/src/space/direction.rs b/crates/tek/src/space/direction.rs new file mode 100644 index 00000000..7ad3eb8d --- /dev/null +++ b/crates/tek/src/space/direction.rs @@ -0,0 +1,28 @@ +use crate::*; + +#[derive(Copy, Clone, PartialEq)] +pub enum Direction { Up, Down, Left, Right, } +impl Direction { + pub fn is_up (&self) -> bool { match self { Self::Up => true, _ => false } } + pub fn is_down (&self) -> bool { match self { Self::Down => true, _ => false } } + pub fn is_left (&self) -> bool { match self { Self::Left => true, _ => false } } + pub fn is_right (&self) -> bool { match self { Self::Right => true, _ => false } } + /// Return next direction clockwise + pub fn cw (&self) -> Self { + match self { + Self::Up => Self::Right, + Self::Down => Self::Left, + Self::Left => Self::Up, + Self::Right => Self::Down, + } + } + /// Return next direction counterclockwise + pub fn ccw (&self) -> Self { + match self { + Self::Up => Self::Left, + Self::Down => Self::Right, + Self::Left => Self::Down, + Self::Right => Self::Up, + } + } +} diff --git a/crates/tek/src/space/size.rs b/crates/tek/src/space/size.rs new file mode 100644 index 00000000..5fe0ed40 --- /dev/null +++ b/crates/tek/src/space/size.rs @@ -0,0 +1,26 @@ +use crate::*; + +pub trait Size { + fn x (&self) -> N; + fn y (&self) -> N; + #[inline] fn w (&self) -> N { self.x() } + #[inline] fn h (&self) -> N { self.y() } + #[inline] fn wh (&self) -> [N;2] { [self.x(), self.y()] } + #[inline] fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w.into()), self.h()] } + #[inline] fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h.into())] } + #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { + if self.w() < w || self.h() < h { + Err(format!("min {w}x{h}").into()) + } else { + Ok(self) + } + } +} +impl Size for (N, N) { + fn x (&self) -> N { self.0 } + fn y (&self) -> N { self.1 } +} +impl Size for [N;2] { + fn x (&self) -> N { self[0] } + fn y (&self) -> N { self[1] } +} diff --git a/crates/tek/src/time.rs b/crates/tek/src/time.rs index c8e61490..58c3c798 100644 --- a/crates/tek/src/time.rs +++ b/crates/tek/src/time.rs @@ -1,350 +1,13 @@ -pub(crate) mod perf; pub(crate) use perf::*; -pub(crate) mod clock; pub(crate) use clock::*; -pub(crate) use self::clock::ClockCommand; - -use crate::*; -use std::iter::Iterator; - -pub const DEFAULT_PPQ: f64 = 96.0; -/// FIXME: remove this and use PPQ from timebase everywhere: -pub const PPQ: usize = 96; - -/// A unit of time, represented as an atomic 64-bit float. -/// -/// According to https://stackoverflow.com/a/873367, as per IEEE754, -/// every integer between 1 and 2^53 can be represented exactly. -/// This should mean that, even at 192kHz sampling rate, over 1 year of audio -/// can be clocked in microseconds with f64 without losing precision. -pub trait TimeUnit { - /// Returns current value - fn get (&self) -> f64; - /// Sets new value, returns old - fn set (&self, value: f64) -> f64; -} -/// Implement arithmetic for a unit of time -macro_rules! impl_op { - ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { - let $a = self.get(); let $b = other.get(); Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { - let $a = self.get(); let $b = other as f64; Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { - let $a = self.get(); let $b = other; Self($impl.into()) - } - } - } -} -/// Define and implement a unit of time -macro_rules! impl_time_unit { - ($T:ident) => { - impl TimeUnit for $T { - fn get (&self) -> f64 { self.0.load(Ordering::Relaxed) } - fn set (&self, value: f64) -> f64 { - let old = self.get(); - self.0.store(value, Ordering::Relaxed); - old - } - } - impl_op!($T, Add, add, |a, b|{a + b}); - impl_op!($T, Sub, sub, |a, b|{a - b}); - impl_op!($T, Mul, mul, |a, b|{a * b}); - impl_op!($T, Div, div, |a, b|{a / b}); - impl_op!($T, Rem, rem, |a, b|{a % b}); - impl From for $T { fn from (value: f64) -> Self { Self(value.into()) } } - impl From for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } } - impl Into for $T { fn into (self) -> f64 { self.get() } } - impl Into for $T { fn into (self) -> usize { self.get() as usize } } - impl Into for &$T { fn into (self) -> f64 { self.get() } } - impl Into for &$T { fn into (self) -> usize { self.get() as usize } } - impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } - } -} - -/// Audio sample rate in Hz (samples per second) -#[derive(Debug, Default)] pub struct SampleRate(AtomicF64); -impl_time_unit!(SampleRate); -impl SampleRate { - /// Return the duration of a sample in microseconds (floating) - #[inline] pub fn usec_per_sample (&self) -> f64 { - 1_000_000f64 / self.get() - } - /// Return the duration of a sample in microseconds (floating) - #[inline] pub fn sample_per_usec (&self) -> f64 { - self.get() / 1_000_000f64 - } - /// Convert a number of samples to microseconds (floating) - #[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 { - self.usec_per_sample() * samples - } - /// Convert a number of microseconds to samples (floating) - #[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 { - self.sample_per_usec() * usecs - } -} - -/// Tempo in beats per minute -#[derive(Debug, Default)] pub struct BeatsPerMinute(AtomicF64); -impl_time_unit!(BeatsPerMinute); - -/// MIDI resolution in PPQ (pulses per quarter note) -#[derive(Debug, Default)] pub struct PulsesPerQuaver(AtomicF64); -impl_time_unit!(PulsesPerQuaver); - -/// Timestamp in microseconds -#[derive(Debug, Default)] pub struct Microsecond(AtomicF64); -impl_time_unit!(Microsecond); -impl Microsecond { - #[inline] pub fn format_msu (&self) -> String { - let usecs = self.get() as usize; - let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); - let (minutes, seconds) = (seconds / 60, seconds % 60); - format!("{minutes}:{seconds:02}:{msecs:03}") - } -} - -/// Timestamp in audio samples -#[derive(Debug, Default)] pub struct SampleCount(AtomicF64); -impl_time_unit!(SampleCount); - -/// Timestamp in MIDI pulses -#[derive(Debug, Default)] pub struct Pulse(AtomicF64); -impl_time_unit!(Pulse); - -/// Quantization setting for launching clips -#[derive(Debug, Default)] pub struct LaunchSync(AtomicF64); -impl_time_unit!(LaunchSync); -impl LaunchSync { - pub fn next (&self) -> f64 { - next_note_length(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - prev_note_length(self.get() as usize) as f64 - } -} - -/// Quantization setting for notes -#[derive(Debug, Default)] pub struct Quantize(AtomicF64); -impl_time_unit!(Quantize); -impl Quantize { - pub fn next (&self) -> f64 { - next_note_length(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - prev_note_length(self.get() as usize) as f64 - } -} - -/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) -#[derive(Debug, Clone)] -pub struct Timebase { - /// Audio samples per second - pub sr: SampleRate, - /// MIDI beats per minute - pub bpm: BeatsPerMinute, - /// MIDI ticks per beat - pub ppq: PulsesPerQuaver, -} -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. - #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator { - TicksIterator { spp: self.samples_per_pulse(), sample: start, start, end } - } - /// Return the duration fo a beat in microseconds - #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } - /// Return the number of beats in a second - #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } - /// Return the number of microseconds corresponding to a note of the given duration - #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { - 4.0 * self.usec_per_beat() * num / den - } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } - /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) - #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } - /// Return number of pulses in a second (BPM-dependent) - #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } - /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) - #[inline] pub fn pulses_per_sample (&self) -> f64 { - self.usec_per_pulse() / self.sr.usec_per_sample() - } - /// Return number of samples in a pulse (SR- and BPM-dependent) - #[inline] pub fn samples_per_pulse (&self) -> f64 { - self.sr.get() / self.pulses_per_second() - } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { - self.pulses_per_sample() * p - } - /// Convert a number of samples to a pulse number (SR- and BPM-dependent) - #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { - s / self.pulses_per_sample() - } - /// Return the number of samples corresponding to a note of the given duration - #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { - self.usec_to_sample(self.note_to_usec(note)) - } - /// Return the number of samples corresponding to the given number of microseconds - #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { - usec * self.sr.get() / 1000f64 - } - /// Return the quantized position of a moment in time given a step - #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { - let step = self.note_to_usec(step); - (time / step, time % step) - } - /// Quantize a collection of events - #[inline] pub fn quantize_into + Sized, T> ( - &self, step: (f64, f64), events: E - ) -> Vec<(f64, f64)> { - events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 0 - #[inline] pub fn format_beats_0 (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - format!("{}.{}.{pulses:02}", beats / 4, beats % 4) - } - /// Format a number of pulses into Beat.Bar starting from 0 - #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4, beats % 4) - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1 (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - format!("{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4 + 1, beats % 4 + 1) - } -} -impl Default for Timebase { - fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) } -} - -#[derive(Debug, Clone)] -pub enum Moment2 { - None, - Zero, - Usec(Microsecond), - Sample(SampleCount), - Pulse(Pulse), -} - -/// A point in time in all time scales (microsecond, sample, MIDI pulse) -#[derive(Debug, Default, Clone)] -pub struct Moment { - pub timebase: Arc, - /// Current time in microseconds - pub usec: Microsecond, - /// Current time in audio samples - pub sample: SampleCount, - /// Current time in MIDI pulses - pub pulse: Pulse, -} -impl Moment { - pub fn zero (timebase: &Arc) -> Self { - Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } - } - pub fn from_usec (timebase: &Arc, usec: f64) -> Self { - Self { - usec: usec.into(), - sample: timebase.sr.usecs_to_sample(usec).into(), - pulse: timebase.usecs_to_pulse(usec).into(), - timebase: timebase.clone(), - } - } - pub fn from_sample (timebase: &Arc, sample: f64) -> Self { - Self { - sample: sample.into(), - usec: timebase.sr.samples_to_usec(sample).into(), - pulse: timebase.samples_to_pulse(sample).into(), - timebase: timebase.clone(), - } - } - pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { - Self { - pulse: pulse.into(), - sample: timebase.pulses_to_sample(pulse).into(), - usec: timebase.pulses_to_usec(pulse).into(), - timebase: timebase.clone(), - } - } - #[inline] pub fn update_from_usec (&self, usec: f64) { - self.usec.set(usec); - self.pulse.set(self.timebase.usecs_to_pulse(usec)); - self.sample.set(self.timebase.sr.usecs_to_sample(usec)); - } - #[inline] pub fn update_from_sample (&self, sample: f64) { - self.usec.set(self.timebase.sr.samples_to_usec(sample)); - self.pulse.set(self.timebase.samples_to_pulse(sample)); - self.sample.set(sample); - } - #[inline] pub fn update_from_pulse (&self, pulse: f64) { - self.usec.set(self.timebase.pulses_to_usec(pulse)); - self.pulse.set(pulse); - self.sample.set(self.timebase.pulses_to_sample(pulse)); - } - #[inline] pub fn format_beat (&self) -> String { - self.timebase.format_beats_1(self.pulse.get()) - } -} -/// Iterator that emits subsequent ticks within a range. -pub struct TicksIterator { - spp: 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 spp = self.spp; - let sample = self.sample as f64; - let start = self.start; - let end = self.end; - self.sample += 1; - //println!("{spp} {sample} {start} {end}"); - let jitter = sample.rem_euclid(spp); // ramps - let next_jitter = (sample + 1.0).rem_euclid(spp); - if jitter > next_jitter { // at crossing: - let time = (sample as usize) % (end as usize-start as usize); - let tick = (sample / spp) as usize; - return Some((time, tick)) - } - } - } -} +pub(crate) mod bpm; pub(crate) use bpm::*; +pub(crate) mod clock; pub(crate) use clock::*; +pub(crate) mod moment; pub(crate) use moment::*; +pub(crate) mod perf; pub(crate) use perf::*; +pub(crate) mod ppq; pub(crate) use ppq::*; +pub(crate) mod quant; pub(crate) use quant::*; +pub(crate) mod sr; pub(crate) use sr::*; +pub(crate) mod timebase; pub(crate) use timebase::*; +pub(crate) mod unit; pub(crate) use unit::*; +pub(crate) mod usec; pub(crate) use usec::*; /// (pulses, name), assuming 96 PPQ pub const NOTE_DURATIONS: [(usize, &str);26] = [ diff --git a/crates/tek/src/time/bpm.rs b/crates/tek/src/time/bpm.rs new file mode 100644 index 00000000..30fffb9d --- /dev/null +++ b/crates/tek/src/time/bpm.rs @@ -0,0 +1,5 @@ +use crate::*; + +/// Tempo in beats per minute +#[derive(Debug, Default)] pub struct BeatsPerMinute(AtomicF64); +impl_time_unit!(BeatsPerMinute); diff --git a/crates/tek/src/time/moment.rs b/crates/tek/src/time/moment.rs new file mode 100644 index 00000000..aafaef6a --- /dev/null +++ b/crates/tek/src/time/moment.rs @@ -0,0 +1,69 @@ +use crate::*; + +#[derive(Debug, Clone)] +pub enum Moment2 { + None, + Zero, + Usec(Microsecond), + Sample(SampleCount), + Pulse(Pulse), +} + +/// A point in time in all time scales (microsecond, sample, MIDI pulse) +#[derive(Debug, Default, Clone)] +pub struct Moment { + pub timebase: Arc, + /// Current time in microseconds + pub usec: Microsecond, + /// Current time in audio samples + pub sample: SampleCount, + /// Current time in MIDI pulses + pub pulse: Pulse, +} +impl Moment { + pub fn zero (timebase: &Arc) -> Self { + Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } + } + pub fn from_usec (timebase: &Arc, usec: f64) -> Self { + Self { + usec: usec.into(), + sample: timebase.sr.usecs_to_sample(usec).into(), + pulse: timebase.usecs_to_pulse(usec).into(), + timebase: timebase.clone(), + } + } + pub fn from_sample (timebase: &Arc, sample: f64) -> Self { + Self { + sample: sample.into(), + usec: timebase.sr.samples_to_usec(sample).into(), + pulse: timebase.samples_to_pulse(sample).into(), + timebase: timebase.clone(), + } + } + pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { + Self { + pulse: pulse.into(), + sample: timebase.pulses_to_sample(pulse).into(), + usec: timebase.pulses_to_usec(pulse).into(), + timebase: timebase.clone(), + } + } + #[inline] pub fn update_from_usec (&self, usec: f64) { + self.usec.set(usec); + self.pulse.set(self.timebase.usecs_to_pulse(usec)); + self.sample.set(self.timebase.sr.usecs_to_sample(usec)); + } + #[inline] pub fn update_from_sample (&self, sample: f64) { + self.usec.set(self.timebase.sr.samples_to_usec(sample)); + self.pulse.set(self.timebase.samples_to_pulse(sample)); + self.sample.set(sample); + } + #[inline] pub fn update_from_pulse (&self, pulse: f64) { + self.usec.set(self.timebase.pulses_to_usec(pulse)); + self.pulse.set(pulse); + self.sample.set(self.timebase.pulses_to_sample(pulse)); + } + #[inline] pub fn format_beat (&self) -> String { + self.timebase.format_beats_1(self.pulse.get()) + } +} diff --git a/crates/tek/src/time/ppq.rs b/crates/tek/src/time/ppq.rs new file mode 100644 index 00000000..964b36eb --- /dev/null +++ b/crates/tek/src/time/ppq.rs @@ -0,0 +1,42 @@ +use crate::*; + +pub const DEFAULT_PPQ: f64 = 96.0; +/// FIXME: remove this and use PPQ from timebase everywhere: +pub const PPQ: usize = 96; + +/// MIDI resolution in PPQ (pulses per quarter note) +#[derive(Debug, Default)] pub struct PulsesPerQuaver(AtomicF64); +impl_time_unit!(PulsesPerQuaver); + +/// Timestamp in MIDI pulses +#[derive(Debug, Default)] pub struct Pulse(AtomicF64); +impl_time_unit!(Pulse); + +/// Iterator that emits subsequent ticks within a range. +pub struct TicksIterator { + pub spp: f64, + pub sample: usize, + pub start: usize, + pub end: usize, +} +impl Iterator for TicksIterator { + type Item = (usize, usize); + fn next (&mut self) -> Option { + loop { + if self.sample > self.end { return None } + let spp = self.spp; + let sample = self.sample as f64; + let start = self.start; + let end = self.end; + self.sample += 1; + //println!("{spp} {sample} {start} {end}"); + let jitter = sample.rem_euclid(spp); // ramps + let next_jitter = (sample + 1.0).rem_euclid(spp); + if jitter > next_jitter { // at crossing: + let time = (sample as usize) % (end as usize-start as usize); + let tick = (sample / spp) as usize; + return Some((time, tick)) + } + } + } +} diff --git a/crates/tek/src/time/quant.rs b/crates/tek/src/time/quant.rs new file mode 100644 index 00000000..dc05e271 --- /dev/null +++ b/crates/tek/src/time/quant.rs @@ -0,0 +1,25 @@ +use crate::*; + +/// Quantization setting for launching clips +#[derive(Debug, Default)] pub struct LaunchSync(AtomicF64); +impl_time_unit!(LaunchSync); +impl LaunchSync { + pub fn next (&self) -> f64 { + next_note_length(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + prev_note_length(self.get() as usize) as f64 + } +} + +/// Quantization setting for notes +#[derive(Debug, Default)] pub struct Quantize(AtomicF64); +impl_time_unit!(Quantize); +impl Quantize { + pub fn next (&self) -> f64 { + next_note_length(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + prev_note_length(self.get() as usize) as f64 + } +} diff --git a/crates/tek/src/time/sr.rs b/crates/tek/src/time/sr.rs new file mode 100644 index 00000000..1d3edd75 --- /dev/null +++ b/crates/tek/src/time/sr.rs @@ -0,0 +1,27 @@ +use crate::*; + +/// Audio sample rate in Hz (samples per second) +#[derive(Debug, Default)] pub struct SampleRate(AtomicF64); +impl_time_unit!(SampleRate); +impl SampleRate { + /// Return the duration of a sample in microseconds (floating) + #[inline] pub fn usec_per_sample (&self) -> f64 { + 1_000_000f64 / self.get() + } + /// Return the duration of a sample in microseconds (floating) + #[inline] pub fn sample_per_usec (&self) -> f64 { + self.get() / 1_000_000f64 + } + /// Convert a number of samples to microseconds (floating) + #[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 { + self.usec_per_sample() * samples + } + /// Convert a number of microseconds to samples (floating) + #[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 { + self.sample_per_usec() * usecs + } +} + +/// Timestamp in audio samples +#[derive(Debug, Default)] pub struct SampleCount(AtomicF64); +impl_time_unit!(SampleCount); diff --git a/crates/tek/src/time/timebase.rs b/crates/tek/src/time/timebase.rs new file mode 100644 index 00000000..cc898b92 --- /dev/null +++ b/crates/tek/src/time/timebase.rs @@ -0,0 +1,112 @@ +use crate::*; + +/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) +#[derive(Debug, Clone)] +pub struct Timebase { + /// Audio samples per second + pub sr: SampleRate, + /// MIDI beats per minute + pub bpm: BeatsPerMinute, + /// MIDI ticks per beat + pub ppq: PulsesPerQuaver, +} + +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. + #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator { + TicksIterator { spp: self.samples_per_pulse(), sample: start, start, end } + } + /// Return the duration fo a beat in microseconds + #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } + /// Return the number of beats in a second + #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } + /// Return the number of microseconds corresponding to a note of the given duration + #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { + 4.0 * self.usec_per_beat() * num / den + } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } + /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) + #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } + /// Return number of pulses in a second (BPM-dependent) + #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } + /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) + #[inline] pub fn pulses_per_sample (&self) -> f64 { + self.usec_per_pulse() / self.sr.usec_per_sample() + } + /// Return number of samples in a pulse (SR- and BPM-dependent) + #[inline] pub fn samples_per_pulse (&self) -> f64 { + self.sr.get() / self.pulses_per_second() + } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { + self.pulses_per_sample() * p + } + /// Convert a number of samples to a pulse number (SR- and BPM-dependent) + #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { + s / self.pulses_per_sample() + } + /// Return the number of samples corresponding to a note of the given duration + #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { + self.usec_to_sample(self.note_to_usec(note)) + } + /// Return the number of samples corresponding to the given number of microseconds + #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { + usec * self.sr.get() / 1000f64 + } + /// Return the quantized position of a moment in time given a step + #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { + let step = self.note_to_usec(step); + (time / step, time % step) + } + /// Quantize a collection of events + #[inline] pub fn quantize_into + Sized, T> ( + &self, step: (f64, f64), events: E + ) -> Vec<(f64, f64)> { + events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 0 + #[inline] pub fn format_beats_0 (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + format!("{}.{}.{pulses:02}", beats / 4, beats % 4) + } + /// Format a number of pulses into Beat.Bar starting from 0 + #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4, beats % 4) + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1 (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + format!("{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4 + 1, beats % 4 + 1) + } +} + +impl Default for Timebase { + fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) } +} diff --git a/crates/tek/src/time/unit.rs b/crates/tek/src/time/unit.rs new file mode 100644 index 00000000..a0ad30e7 --- /dev/null +++ b/crates/tek/src/time/unit.rs @@ -0,0 +1,61 @@ +use crate::*; + +/// A unit of time, represented as an atomic 64-bit float. +/// +/// According to https://stackoverflow.com/a/873367, as per IEEE754, +/// every integer between 1 and 2^53 can be represented exactly. +/// This should mean that, even at 192kHz sampling rate, over 1 year of audio +/// can be clocked in microseconds with f64 without losing precision. +pub trait TimeUnit { + /// Returns current value + fn get (&self) -> f64; + /// Sets new value, returns old + fn set (&self, value: f64) -> f64; +} + +/// Implement arithmetic for a unit of time +#[macro_export] macro_rules! impl_op { + ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { + let $a = self.get(); let $b = other.get(); Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { + let $a = self.get(); let $b = other as f64; Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { + let $a = self.get(); let $b = other; Self($impl.into()) + } + } + } +} + +/// Define and implement a unit of time +#[macro_export] macro_rules! impl_time_unit { + ($T:ident) => { + impl TimeUnit for $T { + fn get (&self) -> f64 { self.0.load(Ordering::Relaxed) } + fn set (&self, value: f64) -> f64 { + let old = self.get(); + self.0.store(value, Ordering::Relaxed); + old + } + } + impl_op!($T, Add, add, |a, b|{a + b}); + impl_op!($T, Sub, sub, |a, b|{a - b}); + impl_op!($T, Mul, mul, |a, b|{a * b}); + impl_op!($T, Div, div, |a, b|{a / b}); + impl_op!($T, Rem, rem, |a, b|{a % b}); + impl From for $T { fn from (value: f64) -> Self { Self(value.into()) } } + impl From for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } } + impl Into for $T { fn into (self) -> f64 { self.get() } } + impl Into for $T { fn into (self) -> usize { self.get() as usize } } + impl Into for &$T { fn into (self) -> f64 { self.get() } } + impl Into for &$T { fn into (self) -> usize { self.get() as usize } } + impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } + } +} diff --git a/crates/tek/src/time/usec.rs b/crates/tek/src/time/usec.rs new file mode 100644 index 00000000..8f768861 --- /dev/null +++ b/crates/tek/src/time/usec.rs @@ -0,0 +1,13 @@ +use crate::*; + +/// Timestamp in microseconds +#[derive(Debug, Default)] pub struct Microsecond(AtomicF64); +impl_time_unit!(Microsecond); +impl Microsecond { + #[inline] pub fn format_msu (&self) -> String { + let usecs = self.get() as usize; + let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); + let (minutes, seconds) = (seconds / 60, seconds % 60); + format!("{minutes}:{seconds:02}:{msecs:03}") + } +}