wip: modularize once again

This commit is contained in:
🪞👃🪞 2025-01-08 18:50:15 +01:00
parent e08c9d1790
commit 3b6ff81dad
44 changed files with 984 additions and 913 deletions

13
time/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "tek_time"
edition = "2021"
version = "0.2.0"
[dependencies]
tek_tui = { path = "../tui" }
tek_jack = { path = "../jack" }
atomic_float = "1.0.0"
quanta = "0.12.3"
#jack = { path = "../rust-jack" }
#midly = "0.5"

132
time/src/clock_tui.rs Normal file
View file

@ -0,0 +1,132 @@
use crate::*;
use ClockCommand::{Play, Pause, SetBpm, SetQuant, SetSync};
//use FocusCommand::{Next, Prev};
use KeyCode::{Enter, Left, Right, Char};
/// Transport clock app.
pub struct TransportTui {
pub jack: Arc<RwLock<JackConnection>>,
pub clock: Clock,
}
handle!(TuiIn: |self: TransportTui, input|ClockCommand::execute_with_state(self, input.event()));
keymap!(TRANSPORT_KEYS = |state: TransportTui, input: Event| ClockCommand {
key(Char(' ')) =>
if state.clock().is_stopped() { Play(None) } else { Pause(None) },
shift(key(Char(' '))) =>
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
});
has_clock!(|self: TransportTui|&self.clock);
audio!(|self: TransportTui, client, scope|ClockAudio(self).process(client, scope));
render!(TuiOut: (self: TransportTui) => TransportView {
compact: false,
clock: &self.clock
});
pub struct TransportView<'a> { pub compact: bool, pub clock: &'a Clock }
impl<'a> TransportView<'a> {
pub fn new (compact: bool, clock: &'a Clock) -> Self {
Self { compact, clock }
}
}
render!(TuiOut: (self: TransportView<'a>) => Outer(
Style::default().fg(TuiTheme::g(255))
).enclose(row!(
OutputStats::new(self.compact, self.clock),
" ",
PlayPause { compact: false, playing: self.clock.is_rolling() },
" ",
BeatStats::new(self.compact, self.clock),
)));
pub struct PlayPause { pub compact: bool, pub playing: bool }
render!(TuiOut: (self: PlayPause) => Tui::bg(
if self.playing{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)},
Either(self.compact,
Thunk::new(||Fixed::x(9, Either(self.playing,
Tui::fg(Color::Rgb(0, 255, 0), " PLAYING "),
Tui::fg(Color::Rgb(255, 128, 0), " STOPPED ")))),
Thunk::new(||Fixed::x(5, Either(self.playing,
Tui::fg(Color::Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
Tui::fg(Color::Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))))));
pub struct BeatStats { compact: bool, bpm: Arc<str>, beat: Arc<str>, time: Arc<str>, }
impl BeatStats {
fn new (compact: bool, clock: &Clock) -> Self {
let bpm = format!("{:.3}", clock.timebase.bpm.get()).into();
let (beat, time) = if let Some(started) = clock.started.read().unwrap().as_ref() {
let now = clock.global.usec.get() - started.usec.get();
(
clock.timebase.format_beats_1(clock.timebase.usecs_to_pulse(now)).into(),
format!("{:.3}s", now/1000000.).into()
)
} else {
("-.-.--".to_string().into(), "-.---s".to_string().into())
};
Self { compact, bpm, beat, time }
}
}
render!(TuiOut: (self: BeatStats) => Either(self.compact,
row!(
FieldV(TuiTheme::g(128).into(), "BPM", &self.bpm),
FieldV(TuiTheme::g(128).into(), "Beat", &self.beat),
FieldV(TuiTheme::g(128).into(), "Time", &self.time),
),
col!(
Bsp::e(Tui::fg(TuiTheme::g(255), &self.bpm), " BPM"),
Bsp::e("Beat ", Tui::fg(TuiTheme::g(255), &self.beat)),
Bsp::e("Time ", Tui::fg(TuiTheme::g(255), &self.time)),
)));
pub struct OutputStats { compact: bool, sample_rate: Arc<str>, buffer_size: Arc<str>, latency: Arc<str>, }
impl OutputStats {
fn new (compact: bool, clock: &Clock) -> Self {
let rate = clock.timebase.sr.get();
let chunk = clock.chunk.load(Relaxed);
Self {
compact,
sample_rate: if compact {
format!("{:.1}kHz", rate / 1000.)
} else {
format!("{:.0}Hz", rate)
}.into(),
buffer_size: format!("{chunk}").into(),
latency: format!("{:.1}ms", chunk as f64 / rate * 1000.).into(),
}
}
}
render!(TuiOut: (self: OutputStats) => Either(self.compact,
row!(
FieldV(TuiTheme::g(128).into(), "SR", &self.sample_rate),
FieldV(TuiTheme::g(128).into(), "Buf", &self.buffer_size),
FieldV(TuiTheme::g(128).into(), "Lat", &self.latency),
),
col!(
Bsp::e(Tui::fg(TuiTheme::g(255), format!("{}", self.sample_rate)), " sample rate"),
Bsp::e(Tui::fg(TuiTheme::g(255), format!("{}", self.buffer_size)), " sample buffer"),
Bsp::e(Tui::fg(TuiTheme::g(255), format!("{:.3}ms", self.latency)), " latency"),
)));
// TODO:
//keymap!(TRANSPORT_BPM_KEYS = |state: Clock, input: Event| ClockCommand {
//key(Char(',')) => SetBpm(state.bpm().get() - 1.0),
//key(Char('.')) => SetBpm(state.bpm().get() + 1.0),
//key(Char('<')) => SetBpm(state.bpm().get() - 0.001),
//key(Char('>')) => SetBpm(state.bpm().get() + 0.001),
//});
//keymap!(TRANSPORT_QUANT_KEYS = |state: Clock, input: Event| ClockCommand {
//key(Char(',')) => SetQuant(state.quant.prev()),
//key(Char('.')) => SetQuant(state.quant.next()),
//key(Char('<')) => SetQuant(state.quant.prev()),
//key(Char('>')) => SetQuant(state.quant.next()),
//});
//keymap!(TRANSPORT_SYNC_KEYS = |sync: Clock, input: Event | ClockCommand {
//key(Char(',')) => SetSync(state.sync.prev()),
//key(Char('.')) => SetSync(state.sync.next()),
//key(Char('<')) => SetSync(state.sync.prev()),
//key(Char('>')) => SetSync(state.sync.next()),
//});
//keymap!(TRANSPORT_SEEK_KEYS = |state: Clock, input: Event| ClockCommand {
//key(Char(',')) => todo!("transport seek bar"),
//key(Char('.')) => todo!("transport seek bar"),
//key(Char('<')) => todo!("transport seek beat"),
//key(Char('>')) => todo!("transport seek beat"),
//});

240
time/src/lib.rs Normal file
View file

@ -0,0 +1,240 @@
mod clock_tui; pub use self::clock_tui::*;
mod microsecond; pub use self::microsecond::*;
mod moment; pub use self::moment::*;
mod perf; pub use self::perf::*;
mod pulse; pub use self::pulse::*;
mod sample_count; pub use self::sample_count::*;
mod sample_rate; pub use self::sample_rate::*;
mod timebase; pub use self::timebase::*;
mod unit; pub use self::unit::*;
pub(crate) use ::tek_jack::{*, jack::{*, contrib::*}};
pub(crate) use std::sync::{Arc, Mutex, RwLock, atomic::{AtomicUsize, Ordering::*}};
pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
pub use ::atomic_float; pub(crate) use atomic_float::*;
pub(crate) use ::tek_tui::{
*,
tek_input::*,
tek_output::*,
crossterm::event::KeyCode,
ratatui::prelude::*
};
pub trait HasClock: Send + Sync {
fn clock (&self) -> &Clock;
}
#[macro_export] macro_rules! has_clock {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasClock for $Struct $(<$($L),*$($T),*>)? {
fn clock (&$self) -> &Clock { $cb }
}
}
}
/// Hosts the JACK callback for updating the temporal pointer and playback status.
pub struct ClockAudio<'a, T: HasClock>(pub &'a mut T);
impl<T: HasClock> Audio for ClockAudio<'_, T> {
#[inline] fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
self.0.clock().update_from_scope(scope).unwrap();
Control::Continue
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ClockCommand {
Play(Option<u32>),
Pause(Option<u32>),
SeekUsec(f64),
SeekSample(f64),
SeekPulse(f64),
SetBpm(f64),
SetQuant(f64),
SetSync(f64),
}
impl<T: HasClock> Command<T> for ClockCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use ClockCommand::*;
match self {
Play(start) => state.clock().play_from(start)?,
Pause(pause) => state.clock().pause_at(pause)?,
SeekUsec(usec) => state.clock().playhead.update_from_usec(usec),
SeekSample(sample) => state.clock().playhead.update_from_sample(sample),
SeekPulse(pulse) => state.clock().playhead.update_from_pulse(pulse),
SetBpm(bpm) => return Ok(Some(SetBpm(state.clock().timebase().bpm.set(bpm)))),
SetQuant(quant) => return Ok(Some(SetQuant(state.clock().quant.set(quant)))),
SetSync(sync) => return Ok(Some(SetSync(state.clock().sync.set(sync)))),
};
Ok(None)
}
}
#[derive(Clone)]
pub struct Clock {
/// JACK transport handle.
pub transport: Arc<Transport>,
/// Global temporal resolution (shared by [Moment] fields)
pub timebase: Arc<Timebase>,
/// Current global sample and usec (monotonic from JACK clock)
pub global: Arc<Moment>,
/// Global sample and usec at which playback started
pub started: Arc<RwLock<Option<Moment>>>,
/// Playback offset (when playing not from start)
pub offset: Arc<Moment>,
/// Current playhead position
pub playhead: Arc<Moment>,
/// Note quantization factor
pub quant: Arc<Quantize>,
/// Launch quantization factor
pub sync: Arc<LaunchSync>,
/// Size of buffer in samples
pub chunk: Arc<AtomicUsize>,
}
from!(|jack: &Arc<RwLock<JackConnection>>| Clock = {
let jack = jack.read().unwrap();
let chunk = jack.client().buffer_size();
let transport = jack.client().transport();
let timebase = Arc::new(Timebase::default());
Self {
quant: Arc::new(24.into()),
sync: Arc::new(384.into()),
transport: Arc::new(transport),
chunk: Arc::new((chunk as usize).into()),
global: Arc::new(Moment::zero(&timebase)),
playhead: Arc::new(Moment::zero(&timebase)),
offset: Arc::new(Moment::zero(&timebase)),
started: RwLock::new(None).into(),
timebase,
}
});
impl std::fmt::Debug for Clock {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("Clock")
.field("timebase", &self.timebase)
.field("chunk", &self.chunk)
.field("quant", &self.quant)
.field("sync", &self.sync)
.field("global", &self.global)
.field("playhead", &self.playhead)
.field("started", &self.started)
.finish()
}
}
impl Clock {
pub fn timebase (&self) -> &Arc<Timebase> {
&self.timebase
}
/// Current sample rate
pub fn sr (&self) -> &SampleRate {
&self.timebase.sr
}
/// Current tempo
pub fn bpm (&self) -> &BeatsPerMinute {
&self.timebase.bpm
}
/// Current MIDI resolution
pub fn ppq (&self) -> &PulsesPerQuaver {
&self.timebase.ppq
}
/// Next pulse that matches launch sync (for phrase switchover)
pub fn next_launch_pulse (&self) -> usize {
let sync = self.sync.get() as usize;
let pulse = self.playhead.pulse.get() as usize;
if pulse % sync == 0 {
pulse
} else {
(pulse / sync + 1) * sync
}
}
/// Start playing, optionally seeking to a given location beforehand
pub fn play_from (&self, start: Option<u32>) -> Usually<()> {
if let Some(start) = start {
self.transport.locate(start)?;
}
self.transport.start()?;
Ok(())
}
/// Pause, optionally seeking to a given location afterwards
pub fn pause_at (&self, pause: Option<u32>) -> Usually<()> {
self.transport.stop()?;
if let Some(pause) = pause {
self.transport.locate(pause)?;
}
Ok(())
}
/// Is currently paused?
pub fn is_stopped (&self) -> bool {
self.started.read().unwrap().is_none()
}
/// Is currently playing?
pub fn is_rolling (&self) -> bool {
self.started.read().unwrap().is_some()
}
/// Update chunk size
pub fn set_chunk (&self, n_frames: usize) {
self.chunk.store(n_frames, Relaxed);
}
pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> {
// Store buffer length
self.set_chunk(scope.n_frames() as usize);
// Store reported global frame and usec
let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?;
self.global.sample.set(current_frames as f64);
self.global.usec.set(current_usecs as f64);
// If transport has just started or just stopped,
// update starting point:
let mut started = self.started.write().unwrap();
match (self.transport.query_state()?, started.as_ref()) {
(TransportState::Rolling, None) => {
let moment = Moment::zero(&self.timebase);
moment.sample.set(current_frames as f64);
moment.usec.set(current_usecs as f64);
*started = Some(moment);
},
(TransportState::Stopped, Some(_)) => {
*started = None;
},
_ => {}
};
self.playhead.update_from_sample(started.as_ref()
.map(|started|current_frames as f64 - started.sample.get())
.unwrap_or(0.));
Ok(())
}
pub fn bbt (&self) -> PositionBBT {
let pulse = self.playhead.pulse.get() as i32;
let ppq = self.timebase.ppq.get() as i32;
let bpm = self.timebase.bpm.get();
let bar = (pulse / ppq) / 4;
PositionBBT {
bar: 1 + bar,
beat: 1 + (pulse / ppq) % 4,
tick: (pulse % ppq),
bar_start_tick: (bar * 4 * ppq) as f64,
beat_type: 4.,
beats_per_bar: 4.,
beats_per_minute: bpm,
ticks_per_beat: ppq as f64
}
}
}
//#[cfg(test)]
//mod test {
//use super::*;
//#[test]
//fn test_samples_to_ticks () {
//let ticks = Ticks(12.3).between_samples(0, 100).collect::<Vec<_>>();
//println!("{ticks:?}");
//}
//}

15
time/src/microsecond.rs Normal file
View file

@ -0,0 +1,15 @@
use crate::*;
/// Timestamp in microseconds
#[derive(Debug, Default)] pub struct Microsecond(AtomicF64);
impl_time_unit!(Microsecond);
impl Microsecond {
#[inline] pub fn format_msu (&self) -> Arc<str> {
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}").into()
}
}

70
time/src/moment.rs Normal file
View file

@ -0,0 +1,70 @@
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<Timebase>,
/// 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<Timebase>) -> Self {
Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() }
}
pub fn from_usec (timebase: &Arc<Timebase>, 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<Timebase>, 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<Timebase>, 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) -> Arc<str> {
self.timebase.format_beats_1(self.pulse.get()).into()
}
}

58
time/src/perf.rs Normal file
View file

@ -0,0 +1,58 @@
use crate::*;
/// Performance counter
pub struct PerfModel {
pub enabled: bool,
clock: quanta::Clock,
// In nanoseconds
used: AtomicF64,
// In microseconds
period: AtomicF64,
}
pub trait HasPerf {
fn perf (&self) -> &PerfModel;
}
impl Default for PerfModel {
fn default () -> Self {
Self {
enabled: true,
clock: quanta::Clock::new(),
used: Default::default(),
period: Default::default(),
}
}
}
impl PerfModel {
pub fn get_t0 (&self) -> Option<u64> {
if self.enabled {
Some(self.clock.raw())
} else {
None
}
}
pub fn update (&self, t0: Option<u64>, scope: &ProcessScope) {
if let Some(t0) = t0 {
let t1 = self.clock.raw();
self.used.store(
self.clock.delta_as_nanos(t0, t1) as f64,
Relaxed,
);
self.period.store(
scope.cycle_times().unwrap().period_usecs as f64,
Relaxed,
);
}
}
pub fn percentage (&self) -> Option<f64> {
let period = self.period.load(Relaxed) * 1000.0;
if period > 0.0 {
let used = self.used.load(Relaxed);
Some(100.0 * used / period)
} else {
None
}
}
}

71
time/src/pulse.rs Normal file
View file

@ -0,0 +1,71 @@
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);
/// Tempo in beats per minute
#[derive(Debug, Default)] pub struct BeatsPerMinute(AtomicF64);
impl_time_unit!(BeatsPerMinute);
/// Quantization setting for launching clips
#[derive(Debug, Default)] pub struct LaunchSync(AtomicF64);
impl_time_unit!(LaunchSync);
impl LaunchSync {
pub fn next (&self) -> f64 {
Note::next(self.get() as usize) as f64
}
pub fn prev (&self) -> f64 {
Note::prev(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 {
Note::next(self.get() as usize) as f64
}
pub fn prev (&self) -> f64 {
Note::prev(self.get() as usize) as f64
}
}
/// 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<Self::Item> {
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))
}
}
}
}

5
time/src/sample_count.rs Normal file
View file

@ -0,0 +1,5 @@
use crate::*;
/// Timestamp in audio samples
#[derive(Debug, Default)] pub struct SampleCount(AtomicF64);
impl_time_unit!(SampleCount);

23
time/src/sample_rate.rs Normal file
View file

@ -0,0 +1,23 @@
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
}
}

112
time/src/timebase.rs Normal file
View file

@ -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<SampleRate>,
b: impl Into<BeatsPerMinute>,
p: impl Into<PulsesPerQuaver>
) -> 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 <E: Iterator<Item=(f64, f64)> + 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) -> Arc<str> {
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).into()
}
/// Format a number of pulses into Beat.Bar starting from 0
#[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc<str> {
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).into()
}
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
#[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc<str> {
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).into()
}
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
#[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc<str> {
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).into()
}
}
impl Default for Timebase {
fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) }
}

59
time/src/unit.rs Normal file
View file

@ -0,0 +1,59 @@
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: InteriorMutable<f64> {}
/// Implement an arithmetic operation 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<Self> 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<usize> 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<f64> 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 Gettable<f64> for $T {
fn get (&self) -> f64 { self.0.load(Relaxed) }
}
impl InteriorMutable<f64> for $T {
fn set (&self, value: f64) -> f64 {
let old = self.get();
self.0.store(value, Relaxed);
old
}
}
impl TimeUnit for $T {}
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<f64> for $T { fn from (value: f64) -> Self { Self(value.into()) } }
impl From<usize> for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } }
impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } }
impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } }
impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } }
impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } }
impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } }
}
}