main
rs 139 lines 4.22 KB
Raw
1 //! Tempo maths: note-value durations, delay times, bar length, sample counts.
2
3 use anyhow::{Result, bail};
4
5 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
6 pub enum Feel {
7 Straight,
8 Dotted,
9 Triplet,
10 }
11
12 impl Feel {
13 fn factor(self) -> f64 {
14 match self {
15 Feel::Straight => 1.0,
16 Feel::Dotted => 1.5,
17 Feel::Triplet => 2.0 / 3.0,
18 }
19 }
20 }
21
22 /// (name, denominator): 1/1 ... 1/64.
23 pub const NOTE_VALUES: [(&str, u32); 7] = [
24 ("1/1", 1),
25 ("1/2", 2),
26 ("1/4", 4),
27 ("1/8", 8),
28 ("1/16", 16),
29 ("1/32", 32),
30 ("1/64", 64),
31 ];
32
33 pub fn quarter_ms(bpm: f64) -> f64 {
34 60_000.0 / bpm
35 }
36
37 /// Duration in milliseconds of `1/denominator` of a whole note at `bpm` (quarter-note beats).
38 pub fn note_ms(bpm: f64, denominator: u32, feel: Feel) -> f64 {
39 quarter_ms(bpm) * 4.0 / denominator as f64 * feel.factor()
40 }
41
42 pub fn hz_from_ms(ms: f64) -> f64 {
43 1000.0 / ms
44 }
45
46 pub fn samples(ms: f64, sample_rate: f64) -> f64 {
47 ms * sample_rate / 1000.0
48 }
49
50 /// A bar of `numerator/denominator`, in milliseconds. Compound meters (6/8, 12/8) are
51 /// measured in notated note values, so 6/8 is six eighth notes.
52 pub fn bar_ms(bpm: f64, numerator: u32, denominator: u32) -> f64 {
53 numerator as f64 * note_ms(bpm, denominator, Feel::Straight)
54 }
55
56 pub fn parse_time_signature(s: &str) -> Result<(u32, u32)> {
57 let parsed = s.split_once('/').and_then(|(n, d)| {
58 let n: u32 = n.trim().parse().ok()?;
59 let d: u32 = d.trim().parse().ok()?;
60 (n > 0 && d.is_power_of_two() && d <= 64).then_some((n, d))
61 });
62 match parsed {
63 Some(ts) => Ok(ts),
64 None => bail!("bad time signature `{s}` (use e.g. 4/4, 6/8, 7/8)"),
65 }
66 }
67
68 pub fn parse_note_value(s: &str) -> Result<(u32, Feel)> {
69 let t = s.trim().to_lowercase();
70 let (body, feel) = if let Some(b) = t.strip_suffix('.').or_else(|| t.strip_suffix("d")) {
71 (b.to_string(), Feel::Dotted)
72 } else if let Some(b) = t.strip_suffix('t') {
73 (b.to_string(), Feel::Triplet)
74 } else {
75 (t, Feel::Straight)
76 };
77 let denominator = body
78 .strip_prefix("1/")
79 .unwrap_or(&body)
80 .parse::<u32>()
81 .ok()
82 .filter(|d| NOTE_VALUES.iter().any(|(_, n)| n == d));
83 match denominator {
84 Some(d) => Ok((d, feel)),
85 None => bail!("bad note value `{s}` (use 1/4, 1/8., 1/8t, 1/16)"),
86 }
87 }
88
89 /// Tempo from a loop or section of `bars` bars lasting `seconds` seconds.
90 pub fn bpm_from_duration(bars: f64, numerator: u32, denominator: u32, seconds: f64) -> f64 {
91 let quarter_beats = bars * numerator as f64 * 4.0 / denominator as f64;
92 quarter_beats * 60.0 / seconds
93 }
94
95 #[cfg(test)]
96 mod tests {
97 use super::*;
98
99 fn close(a: f64, b: f64) -> bool {
100 (a - b).abs() < 1e-9
101 }
102
103 #[test]
104 fn dotted_eighth_at_120_is_375_ms() {
105 assert!(close(note_ms(120.0, 8, Feel::Dotted), 375.0));
106 assert!(close(note_ms(120.0, 4, Feel::Straight), 500.0));
107 assert!(close(note_ms(120.0, 4, Feel::Triplet), 1000.0 / 3.0));
108 assert!(close(note_ms(120.0, 16, Feel::Straight), 125.0));
109 assert!(close(note_ms(60.0, 1, Feel::Straight), 4000.0));
110 }
111
112 #[test]
113 fn bars_and_samples() {
114 assert!(close(bar_ms(120.0, 4, 4), 2000.0));
115 assert!(close(bar_ms(120.0, 3, 4), 1500.0));
116 assert!(close(bar_ms(120.0, 6, 8), 1500.0));
117 assert!(close(bar_ms(140.0, 7, 8), 7.0 * 60_000.0 / 140.0 / 2.0));
118 assert!(close(samples(375.0, 48_000.0), 18_000.0));
119 assert!(close(hz_from_ms(500.0), 2.0));
120 }
121
122 #[test]
123 fn parsing() {
124 assert_eq!(parse_time_signature("7/8").unwrap(), (7, 8));
125 assert!(parse_time_signature("4/3").is_err());
126 assert!(parse_time_signature("x").is_err());
127 assert_eq!(parse_note_value("1/8.").unwrap(), (8, Feel::Dotted));
128 assert_eq!(parse_note_value("1/4t").unwrap(), (4, Feel::Triplet));
129 assert_eq!(parse_note_value("16").unwrap(), (16, Feel::Straight));
130 assert!(parse_note_value("1/3").is_err());
131 }
132
133 #[test]
134 fn tempo_from_loop_length() {
135 // 4 bars of 4/4 in 8 seconds is 120 bpm.
136 assert!(close(bpm_from_duration(4.0, 4, 4, 8.0), 120.0));
137 assert!(close(bpm_from_duration(2.0, 3, 4, 3.0), 120.0));
138 }
139 }