wip: simplify time traits (f64-first)

This commit is contained in:
🪞👃🪞 2024-11-01 14:55:03 +02:00
parent 66f9afe500
commit 98073dd40c

View file

@ -1,30 +1,94 @@
use crate::*; use crate::*;
use std::iter::Iterator; use std::iter::Iterator;
/// Any numeric type that represents time
pub trait TimeUnit: Copy + Display + PartialOrd + PartialEq pub const DEFAULT_PPQ: f64 = 96.0;
+ Add<Self, Output=Self> + Mul<Self, Output=Self>
+ Div<Self, Output=Self> + Rem<Self, Output=Self> {} /// The unit of time, an atomic 64-bit float.
impl<T> TimeUnit for T where T: Copy + Display + PartialOrd + PartialEq ///
+ Add<Self, Output=Self> + Mul<Self, Output=Self> /// According to https://stackoverflow.com/a/873367, as per IEEE754,
+ Div<Self, Output=Self> + Rem<Self, Output=Self> {} /// every integer between 1 and 2^53 can be represented exactly.
/// Integer time unit, such as samples, pulses, or microseconds /// This should mean that, even at 192kHz sampling rate, over 1 year of audio
pub trait TimeInteger: TimeUnit + From<usize> + Into<usize> + Copy {} /// can be clocked in microseconds with f64 without losing precision.
impl<T> TimeInteger for T where T: TimeUnit + From<usize> + Into<usize> + Copy {} #[derive(Debug, Default)]
/// Floating time unit, such as beats or seconds pub struct TimeUnit(AtomicF64);
pub trait TimeFloat: TimeUnit + From<f64> + Into<f64> + Copy {} /// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat)
impl<T> TimeFloat for T where T: TimeUnit + From<f64> + Into<f64> + Copy {} #[derive(Debug, Clone)]
/// Defines global temporal resolutions. pub struct Timebase {
#[derive(Debug)] pub struct Timebase {
/// Audio samples per second /// Audio samples per second
sr: AtomicF64, pub sr: TimeUnit,
/// MIDI beats per minute /// MIDI beats per minute
bpm: AtomicF64, pub bpm: TimeUnit,
/// MIDI ticks per beat /// MIDI ticks per beat
ppq: AtomicF64, pub ppq: TimeUnit,
} }
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
#[derive(Debug, Default, Clone)]
pub struct Instant {
pub timebase: Arc<Timebase>,
/// Current time in microseconds
pub usec: TimeUnit,
/// Current time in audio samples
pub sample: TimeUnit,
/// Current time in MIDI pulses
pub pulse: TimeUnit,
}
impl From<f64> for TimeUnit {
fn from (value: f64) -> Self { Self(value.into()) }
}
impl From<usize> for TimeUnit {
fn from (value: usize) -> Self { Self((value as f64).into()) }
}
pub trait TimeUnitMethods<T> {
fn get (&self) -> T;
fn set (&self, value: T);
}
impl TimeUnitMethods<f64> for TimeUnit {
fn get (&self) -> f64 { self.0.load(Ordering::Relaxed) }
fn set (&self, value: f64) { self.0.store(value, Ordering::Relaxed) }
}
impl TimeUnitMethods<usize> for TimeUnit {
fn get (&self) -> usize { self.0.load(Ordering::Relaxed) as usize }
fn set (&self, value: usize) { self.0.store(value as f64, Ordering::Relaxed) }
}
impl Clone for TimeUnit {
fn clone (&self) -> Self { Self(TimeUnitMethods::<f64>::get(self).into()) }
}
macro_rules! impl_op {
($Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => {
impl $Op<Self> for TimeUnit {
type Output = Self;
#[inline] fn $method (self, other: Self) -> Self {
let $a = TimeUnitMethods::<f64>::get(&self);
let $b = TimeUnitMethods::<f64>::get(&other);
Self($impl.into())
}
}
impl $Op<usize> for TimeUnit {
type Output = Self;
#[inline] fn $method (self, other: usize) -> Self {
let $a = TimeUnitMethods::<f64>::get(&self);
let $b = other as f64;
Self($impl.into())
}
}
impl $Op<f64> for TimeUnit {
type Output = Self;
#[inline] fn $method (self, other: f64) -> Self {
let $a = TimeUnitMethods::<f64>::get(&self);
let $b = other;
Self($impl.into())
}
}
}
}
impl_op!(Add, add, |a, b|{a + b});
impl_op!(Sub, sub, |a, b|{a - b});
impl_op!(Mul, mul, |a, b|{a * b});
impl_op!(Div, div, |a, b|{a / b});
impl_op!(Rem, rem, |a, b|{a % b});
impl Timebase { impl Timebase {
/// Specify sample rate, BPM and PPQ /// Specify sample rate, BPM and PPQ
pub fn new (s: impl Into<AtomicF64>, b: impl Into<AtomicF64>, p: impl Into<AtomicF64>) -> Self { pub fn new (s: impl Into<TimeUnit>, b: impl Into<TimeUnit>, p: impl Into<TimeUnit>) -> Self {
Self { sr: s.into(), bpm: b.into(), ppq: p.into() } Self { sr: s.into(), bpm: b.into(), ppq: p.into() }
} }
/// Iterate over ticks between start and end. /// Iterate over ticks between start and end.
@ -32,16 +96,7 @@ impl Timebase {
TicksIterator { fpt: self.samples_per_pulse(), sample: start, start, end } TicksIterator { fpt: self.samples_per_pulse(), sample: start, start, end }
} }
} }
/// Represents a point in time in all scales impl Default for Timebase { fn default () -> Self { Self::new(48000f64, 150f64, 96f64) } }
#[derive(Debug, Default)] pub struct Instant {
pub timebase: Arc<Timebase>,
/// Current time in microseconds
usec: AtomicUsize,
/// Current time in audio samples
sample: AtomicUsize,
/// Current time in MIDI pulses
pulse: AtomicF64,
}
impl Instant { impl Instant {
pub fn from_usec (timebase: &Arc<Timebase>, usec: usize) -> Self { pub fn from_usec (timebase: &Arc<Timebase>, usec: usize) -> Self {
Self { Self {
@ -68,22 +123,22 @@ impl Instant {
} }
} }
pub fn update_from_usec (&self, usec: usize) { pub fn update_from_usec (&self, usec: usize) {
self.set_usec(usec); self.usec.set(usec);
self.set_pulse(self.timebase.usecs_to_pulse(usec as f64)); self.pulse.set(self.timebase.usecs_to_pulse(usec as f64));
self.set_sample(self.timebase.usecs_to_sample(usec as f64) as usize); self.sample.set(self.timebase.usecs_to_sample(usec as f64));
} }
pub fn update_from_sample (&self, sample: usize) { pub fn update_from_sample (&self, sample: usize) {
self.set_usec(self.timebase.samples_to_usec(sample as f64) as usize); self.usec.set(self.timebase.samples_to_usec(sample as f64));
self.set_pulse(self.timebase.samples_to_pulse(sample as f64)); self.pulse.set(self.timebase.samples_to_pulse(sample as f64));
self.set_sample(sample); self.sample.set(sample);
} }
pub fn update_from_pulse (&self, pulse: f64) { pub fn update_from_pulse (&self, pulse: f64) {
self.set_usec(self.timebase.pulses_to_usec(pulse) as usize); self.usec.set(self.timebase.pulses_to_usec(pulse));
self.set_pulse(pulse); self.pulse.set(pulse);
self.set_sample(self.timebase.pulses_to_sample(pulse) as usize); self.sample.set(self.timebase.pulses_to_sample(pulse));
} }
pub fn format_beat (&self) -> String { pub fn format_beat (&self) -> String {
self.format_beats_float(self.pulse()) self.format_beats(self.pulse().get())
} }
} }
/// Iterator that emits subsequent ticks within a range. /// Iterator that emits subsequent ticks within a range.
@ -114,229 +169,128 @@ impl Iterator for TicksIterator {
} }
} }
} }
/// Trait for struct that defines a sample rate in hertz (samples per second) /// Something that defines a sample rate in hertz (samples per second)
pub trait SampleRate<U: TimeUnit> { pub trait SampleRate {
/// Get the sample rate /// Get the sample rate
fn sr (&self) -> U; fn sr (&self) -> &TimeUnit;
/// Set the sample rate
fn set_sr (&self, sr: U);
/// Return the duration of a sample in microseconds (floating) /// Return the duration of a sample in microseconds (floating)
#[inline] fn usec_per_sample (&self) -> U where U: TimeFloat { #[inline] fn usec_per_sample (&self) -> TimeUnit {
U::from(1_000_000f64 / self.sr().into()) TimeUnit::from(1_000_000f64) / self.sr().get()
} }
/// Return the duration of a sample in microseconds (floating) /// Return the duration of a sample in microseconds (floating)
#[inline] fn sample_per_usec (&self) -> U where U: TimeFloat { #[inline] fn sample_per_usec (&self) -> TimeUnit {
U::from(self.sr().into() / 1_000_000f64) TimeUnit::from(1_000_000f64) / self.sr().get()
} }
/// Convert a number of samples to microseconds (floating) /// Convert a number of samples to microseconds (floating)
#[inline] fn samples_to_usec (&self, samples: U) -> U where U: TimeFloat { #[inline] fn samples_to_usec (&self, samples: impl Into<TimeUnit>) -> f64 {
samples * self.usec_per_sample() samples.into() * self.usec_per_sample()
} }
/// Convert a number of microseconds to samples (floating) /// Convert a number of microseconds to samples (floating)
#[inline] fn usecs_to_sample (&self, usecs: U) -> U where U: TimeFloat, Self: SampleRate<U> { #[inline] fn usecs_to_sample (&self, usecs: impl Into<TimeUnit>) -> f64 {
usecs * self.sample_per_usec() usecs.into() * self.sample_per_usec()
} }
} }
impl SampleRate<f64> for Timebase { impl SampleRate for Timebase { #[inline] fn sr (&self) -> &TimeUnit { &self.sr } }
#[inline] fn sr (&self) -> f64 { self.sr.load(Ordering::Relaxed) } impl SampleRate for Instant { #[inline] fn sr (&self) -> &TimeUnit { self.timebase.sr() } }
#[inline] fn set_sr (&self, sr: f64) { self.sr.store(sr, Ordering::Relaxed); } /// Something that defines a tempo in beats per minute
} pub trait BeatsPerMinute {
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 /// Get the tempo
fn bpm (&self) -> U; fn bpm (&self) -> &TimeUnit;
/// Set the tempo
fn set_bpm (&self, bpm: U);
/// Return the duration fo a beat in microseconds /// Return the duration fo a beat in microseconds
#[inline] fn usec_per_beat (&self) -> U { #[inline] fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm().get() }
U::from(60_000_000f64) / self.bpm()
}
/// Return the number of beats in a second /// Return the number of beats in a second
#[inline] fn beat_per_second (&self) -> U { #[inline] fn beat_per_second (&self) -> f64 { self.bpm().get() / 60_000_000f64 }
self.bpm() / U::from(60_000_000f64)
}
/// Return the number of microseconds corresponding to a note of the given duration /// Return the number of microseconds corresponding to a note of the given duration
#[inline] fn note_to_usec (&self, (num, den): (U, U)) -> U { #[inline] fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 {
U::from(4.0) * self.usec_per_beat() * num / den 4.0 * self.usec_per_beat() * num / den
} }
/// Return the number of samples corresponding to a note of the given duration /// Return the number of samples corresponding to a note of the given duration
#[inline] fn note_to_samples (&self, note: (U, U)) -> U where Self: SampleRate<U> { #[inline] fn note_to_samples (&self, note: (f64, f64)) -> TimeUnit {
self.usec_to_sample(self.note_to_usec(note)) self.usec_to_sample(self.note_to_usec(note))
} }
/// Return the number of samples corresponding to the given number of microseconds /// Return the number of samples corresponding to the given number of microseconds
#[inline] fn usec_to_sample (&self, usec: U) -> U where Self: SampleRate<U> { #[inline] fn usec_to_sample (&self, usec: f64) -> TimeUnit {
usec * self.sr() / U::from(1000f64) usec * self.sr() / TimeUnit::from(1000f64)
} }
/// Return the quantized position of a moment in time given a step /// Return the quantized position of a moment in time given a step
#[inline] fn quantize (&self, step: (U, U), time: U) -> (U, U) { #[inline] fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) {
let step = self.note_to_usec(step); let step = self.note_to_usec(step);
(time / step, time % step) (time / step, time % step)
} }
/// Quantize a collection of events /// Quantize a collection of events
#[inline] fn quantize_into <E: Iterator<Item=(U, U)> + Sized, T> ( #[inline] fn quantize_into <E: Iterator<Item=(TimeUnit, TimeUnit)> + Sized, T> (
&self, step: (U, U), events: E &self, step: (TimeUnit, TimeUnit), events: E
) -> Vec<(U, U)> { ) -> Vec<(TimeUnit, TimeUnit)> {
events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() events.map(|(time, event)|(self.quantize(step, time).0, event)).collect()
} }
} }
impl BeatsPerMinute<f64> for Timebase { impl BeatsPerMinute for Timebase { #[inline] fn bpm (&self) -> &TimeUnit { &self.bpm } }
#[inline] fn bpm (&self) -> f64 { self.bpm.load(Ordering::Relaxed) } impl BeatsPerMinute for Instant { #[inline] fn bpm (&self) -> &TimeUnit { self.timebase.bpm() } }
#[inline] fn set_bpm (&self, bpm: f64) { self.bpm.store(bpm, Ordering::Relaxed); } /// Something that defines a MIDI resolution in pulses per quaver (beat)
} pub trait PulsesPerQuaver {
impl BeatsPerMinute<f64> for Instant { // Get the PPQ
#[inline] fn bpm (&self) -> f64 { self.timebase.bpm() } fn ppq (&self) -> &TimeUnit;
#[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;
/// Get the PPQ
fn ppq (&self) -> U;
/// Set the PPQ
fn set_ppq (&self, ppq: U);
/// Return duration of a pulse in microseconds (BPM-dependent) /// Return duration of a pulse in microseconds (BPM-dependent)
#[inline] fn pulse_per_usec (&self) -> U #[inline] fn pulse_per_usec (&self) -> f64 { self.ppq().get() / self.usec_per_beat() }
where U: TimeFloat, Self: BeatsPerMinute<U>
{
self.ppq() / self.usec_per_beat()
}
/// Return duration of a pulse in microseconds (BPM-dependent) /// Return duration of a pulse in microseconds (BPM-dependent)
#[inline] fn usec_per_pulse (&self) -> U #[inline] fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq().get() }
where U: TimeFloat, Self: BeatsPerMinute<U>
{
self.usec_per_beat() / self.ppq()
}
/// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent)
#[inline] fn usecs_to_pulse (&self, usec: U) -> U #[inline] fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() }
where U: TimeFloat, Self: BeatsPerMinute<U>
{
usec * self.pulse_per_usec()
}
/// Convert a number of pulses to a sample number (SR- and BPM-dependent) /// Convert a number of pulses to a sample number (SR- and BPM-dependent)
#[inline] fn pulses_to_usec (&self, pulse: U) -> U #[inline] fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() }
where U: TimeFloat, Self: SampleRate<U> + BeatsPerMinute<U>
{
pulse / self.usec_per_pulse()
}
/// Return number of pulses in a second (BPM-dependent) /// Return number of pulses in a second (BPM-dependent)
#[inline] fn pulses_per_second (&self) -> U #[inline] fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq().get() }
where U: TimeFloat, Self: BeatsPerMinute<U>
{
self.beat_per_second() * self.ppq()
}
/// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent)
#[inline] fn pulses_per_sample (&self) -> U #[inline] fn pulses_per_sample (&self) -> f64 { self.usec_per_pulse() / self.usec_per_sample() }
where U: TimeFloat, Self: SampleRate<U> + BeatsPerMinute<U>
{
self.usec_per_pulse() / self.usec_per_sample()
}
/// Convert a number of pulses to a sample number (SR- and BPM-dependent) /// Convert a number of pulses to a sample number (SR- and BPM-dependent)
#[inline] fn pulses_to_sample (&self, p: U) -> U #[inline] fn pulses_to_sample (&self, p: f64) -> f64 { self.pulses_per_sample() * p }
where U: TimeFloat, Self: SampleRate<U> + BeatsPerMinute<U>
{
self.pulses_per_sample() * p
}
/// Convert a number of samples to a pulse number (SR- and BPM-dependent) /// Convert a number of samples to a pulse number (SR- and BPM-dependent)
#[inline] fn samples_to_pulse (&self, s: U) -> U #[inline] fn samples_to_pulse (&self, s: f64) -> f64 { s / self.pulses_per_sample() }
where U: TimeFloat, Self: SampleRate<U> + BeatsPerMinute<U>
{
s / self.pulses_per_sample()
}
/// Return number of samples in a pulse (SR- and BPM-dependent) /// Return number of samples in a pulse (SR- and BPM-dependent)
#[inline] fn samples_per_pulse (&self) -> U #[inline] fn samples_per_pulse (&self) -> f64 { self.sr() / self.pulses_per_second() }
where U: TimeFloat, Self: SampleRate<U> + BeatsPerMinute<U> /// Format a number of pulses into Beat.Bar.Pulse
{ #[inline] fn format_beats (&self, pulse: f64) -> String {
self.sr() / self.pulses_per_second() let ppq: usize = self.ppq().get();
} let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
#[inline] fn format_beats_int (&self, pulse: U) -> String where U: TimeInteger { let bars = ((beats / 4) + 1) as usize;
let ppq = self.ppq(); let beats = ((beats % 4) + 1) as usize;
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}")
}
#[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}") format!("{bars}.{beats}.{pulses:02}")
} }
} }
impl PulsesPerQuaver<f64> for Timebase { impl PulsesPerQuaver for Timebase { #[inline] fn ppq (&self) -> &TimeUnit { &self.ppq } }
const DEFAULT_PPQ: f64 = 96f64; impl PulsesPerQuaver for Instant { #[inline] fn ppq (&self) -> &TimeUnit { self.timebase.ppq() } }
#[inline] fn ppq (&self) -> f64 { self.ppq.load(Ordering::Relaxed) } /// Something that refers to a point in time in samples
#[inline] fn set_ppq (&self, ppq: f64) { self.ppq.store(ppq, Ordering::Relaxed); } pub trait SamplePosition { fn sample (&self) -> &TimeUnit; }
} impl SamplePosition for Instant { #[inline] fn sample (&self) -> &TimeUnit { &self.sample } }
impl PulsesPerQuaver<f64> for Instant { /// Something that refers to a point in time in MIDI pulses
const DEFAULT_PPQ: f64 = 96f64; pub trait PulsePosition { fn pulse (&self) -> &TimeUnit; }
#[inline] fn ppq (&self) -> f64 { self.timebase.ppq() } impl PulsePosition for Instant { #[inline] fn pulse (&self) -> &TimeUnit { &self.pulse } }
#[inline] fn set_ppq (&self, ppq: f64) { self.timebase.set_ppq(ppq); } /// Something that refers to a point in time in microseconds
} pub trait UsecPosition {
pub trait SamplePosition<U: TimeUnit> { fn usec (&self) -> &TimeUnit;
fn sample (&self) -> U; #[inline] fn format_current_usec (&self) -> String {
fn set_sample (&self, sample: U); let usecs: usize = self.usec().get();
} let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000);
impl SamplePosition<usize> for Instant { let (minutes, seconds) = (seconds / 60, seconds % 60);
#[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);
}
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;
fn set_usec (&self, usec: U);
#[inline] fn format_current_usec (&self) -> String where U: From<usize> {
let usecs = self.usec();
let (seconds, msecs) = (usecs / U::from(1000000), usecs / U::from(1000) % U::from(1000));
let (minutes, seconds) = (seconds / U::from(60), seconds % U::from(60));
format!("{minutes}:{seconds:02}:{msecs:03}") format!("{minutes}:{seconds:02}:{msecs:03}")
} }
} }
impl UsecPosition<usize> for Instant { impl UsecPosition for Instant { #[inline] fn usec (&self) -> &TimeUnit { &self.usec } }
#[inline] fn usec (&self) -> usize { self.usec.load(Ordering::Relaxed) } /// Something that defines launch quantization
#[inline] fn set_usec (&self, u: usize) { self.usec.store(u, Ordering::Relaxed); } pub trait LaunchSync {
} fn sync (&self) -> &TimeUnit;
pub trait LaunchSync<U: TimeUnit> { #[inline] fn next_launch_pulse (&self) -> f64 where Self: PulsePosition {
fn sync (&self) -> U; let sync: f64 = self.sync().get();
fn set_sync (&self, sync: U); let pulse = self.pulse().get();
#[inline] fn next_launch_pulse (&self) -> U where U: TimeInteger, Self: PulsePosition<U> { if pulse % sync == 0. {
let sync = self.sync();
let pulse = self.pulse();
if pulse % sync == U::from(0) {
pulse pulse
} else { } else {
(pulse / sync + U::from(1)) * sync (pulse / sync + 1.) * sync
} }
} }
} }
pub trait Quantize<T> { /// Something that defines note quantization
fn quant (&self) -> T; pub trait Quantize<T> { fn quant (&self) -> &TimeUnit; }
fn set_quant (&self, quant: T);
}
impl Default for Timebase { fn default () -> Self { Self::new(48000f64, 150f64, 96f64) } }
/// (pulses, name), assuming 96 PPQ /// (pulses, name), assuming 96 PPQ
pub const NOTE_DURATIONS: [(usize, &str);26] = [ pub const NOTE_DURATIONS: [(usize, &str);26] = [
(1, "1/384"), (1, "1/384"),