1use crate::audio::{self, BeatGrid, Feel, NoteValue};
14use crate::cast;
15use crate::notes::{TICKS_PER_BEAT, ticks_per_second};
16use crate::units::{Bpm, Seconds};
17
18#[must_use]
26pub fn euclid(k: usize, n: usize) -> Vec<bool> {
27 let k = k.min(n);
28 if k == 0 || k == n {
29 return vec![k == n; n];
30 }
31 let mut leading: Vec<Vec<bool>> = vec![vec![true]; k];
32 let mut trailing: Vec<Vec<bool>> = vec![vec![false]; n - k];
33 loop {
34 let pairs = leading.len().min(trailing.len());
35 let remainder = if leading.len() > pairs {
36 leading.split_off(pairs)
37 } else {
38 trailing.split_off(pairs)
39 };
40 for (group, tail) in leading.iter_mut().zip(&trailing) {
41 group.extend(tail);
42 }
43 trailing = remainder;
44 if trailing.len() <= 1 {
45 break;
46 }
47 }
48 leading.into_iter().chain(trailing).flatten().collect()
49}
50
51#[must_use]
53pub fn onsets(pattern: &[bool]) -> Vec<usize> {
54 pattern
55 .iter()
56 .enumerate()
57 .filter_map(|(step, onset)| onset.then_some(step))
58 .collect()
59}
60
61#[must_use]
64pub fn rotate(pattern: &[bool], rotation: usize) -> Vec<bool> {
65 let mut turned = pattern.to_vec();
66 if let Some(steps) = rotation.checked_rem(turned.len()) {
67 turned.rotate_right(steps);
68 }
69 turned
70}
71
72#[must_use]
78pub fn swing_delay(pct: f64) -> Option<f64> {
79 non_negative((pct / 100.0 - 0.5) * 2.0)
80}
81
82#[must_use]
87pub fn swing_ratio(pct: f64) -> Option<f64> {
88 non_negative(pct / (100.0 - pct))
89}
90
91#[must_use]
96pub fn swing_pct(ratio: f64) -> Option<f64> {
97 if ratio < 0.0 {
98 return None;
99 }
100 non_negative(ratio / (ratio + 1.0) * 100.0)
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub enum Pair {
108 Sixteenths,
110 Eighths,
112}
113
114impl Pair {
115 fn ticks(self) -> u32 {
117 let note = match self {
118 Self::Sixteenths => Division::Sixteenth,
119 Self::Eighths => Division::Eighth,
120 };
121 2 * note.ticks()
122 }
123}
124
125#[must_use]
130pub fn swing_ticks(pct: f64, pair: Pair) -> Option<f64> {
131 non_negative((pct / 100.0 - 0.5) * f64::from(pair.ticks()))
132}
133
134#[must_use]
139pub fn tick_ms(bpm: f64) -> Option<f64> {
140 let bpm = Bpm::new(bpm).ok()?;
141 Some(1000.0 / ticks_per_second(bpm))
142}
143
144#[must_use]
149pub fn sixteenth_ms(bpm: f64) -> Option<f64> {
150 let grid = BeatGrid::new(Bpm::new(bpm).ok()?, Seconds::from_const(0.0));
151 Some(grid.beat_period().get() / 4.0 * 1000.0)
152}
153
154fn non_negative(value: f64) -> Option<f64> {
157 (value.is_finite() && value >= 0.0).then_some(value + 0.0)
158}
159
160#[must_use]
166pub fn ticks_for_ms(ms: f64, bpm: f64) -> Option<u32> {
167 let tick = tick_ms(bpm)?;
168 if ms.is_nan() || ms < 0.0 {
169 return None;
170 }
171 let ticks = cast::ceil_to_usize((ms / tick - 1e-9).max(0.0))?;
173 u32::try_from(ticks).ok()
174}
175
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
178pub enum Division {
179 ThirtySecond,
181 Sixteenth,
183 SixteenthTriplet,
185 Eighth,
187 EighthTriplet,
189 DottedEighth,
191 QuarterTriplet,
193 Quarter,
195 DottedQuarter,
197 Bar,
199}
200
201impl Division {
202 pub const ALL: [Self; 10] = [
204 Self::ThirtySecond,
205 Self::SixteenthTriplet,
206 Self::Sixteenth,
207 Self::EighthTriplet,
208 Self::Eighth,
209 Self::QuarterTriplet,
210 Self::DottedEighth,
211 Self::Quarter,
212 Self::DottedQuarter,
213 Self::Bar,
214 ];
215
216 fn division(self) -> audio::Division {
220 let (value, feel) = match self {
221 Self::ThirtySecond => (NoteValue::ThirtySecond, Feel::Straight),
222 Self::Sixteenth => (NoteValue::Sixteenth, Feel::Straight),
223 Self::SixteenthTriplet => (NoteValue::Sixteenth, Feel::Triplet),
224 Self::Eighth => (NoteValue::Eighth, Feel::Straight),
225 Self::EighthTriplet => (NoteValue::Eighth, Feel::Triplet),
226 Self::DottedEighth => (NoteValue::Eighth, Feel::Dotted),
227 Self::QuarterTriplet => (NoteValue::Quarter, Feel::Triplet),
228 Self::Quarter => (NoteValue::Quarter, Feel::Straight),
229 Self::DottedQuarter => (NoteValue::Quarter, Feel::Dotted),
230 Self::Bar => (NoteValue::Whole, Feel::Straight),
231 };
232 audio::Division { value, feel }
233 }
234
235 #[must_use]
238 pub fn ticks(self) -> u32 {
239 let ticks = self.division().beats_per_cycle() * f64::from(TICKS_PER_BEAT);
243 cast::round_to_usize(ticks)
244 .and_then(|ticks| u32::try_from(ticks).ok())
245 .unwrap_or(u32::MAX)
246 }
247
248 #[must_use]
250 pub fn name(self) -> &'static str {
251 match self {
252 Self::ThirtySecond => "1/32",
253 Self::Sixteenth => "1/16",
254 Self::SixteenthTriplet => "1/16T",
255 Self::Eighth => "1/8",
256 Self::EighthTriplet => "1/8T",
257 Self::DottedEighth => "dotted 1/8",
258 Self::QuarterTriplet => "1/4T",
259 Self::Quarter => "1/4",
260 Self::DottedQuarter => "dotted 1/4",
261 Self::Bar => "bar",
262 }
263 }
264}
265
266#[must_use]
272pub fn slot_ticks(grid: u8) -> Option<u32> {
273 let bar = Division::Bar.ticks();
274 let grid = u32::from(grid);
275 (bar.checked_rem(grid)? == 0).then(|| bar / grid)
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 const PRINTED: [(usize, usize, &[usize]); 20] = [
284 (2, 5, &[0, 2]),
285 (3, 4, &[0, 2, 3]),
286 (3, 5, &[0, 2, 4]),
287 (3, 7, &[0, 2, 4]),
288 (3, 8, &[0, 3, 6]),
289 (4, 7, &[0, 2, 4, 6]),
290 (4, 9, &[0, 2, 4, 6]),
291 (4, 11, &[0, 3, 6, 9]),
292 (5, 6, &[0, 2, 3, 4, 5]),
293 (5, 7, &[0, 2, 3, 5, 6]),
294 (5, 8, &[0, 2, 3, 5, 6]),
295 (5, 9, &[0, 2, 4, 6, 8]),
296 (5, 11, &[0, 2, 4, 6, 8]),
297 (5, 12, &[0, 3, 5, 8, 10]),
298 (5, 13, &[0, 3, 5, 8, 10]),
299 (7, 8, &[0, 2, 3, 4, 5, 6, 7]),
300 (7, 12, &[0, 2, 3, 5, 7, 8, 10]),
301 (5, 16, &[0, 3, 6, 9, 12]),
302 (7, 16, &[0, 3, 5, 7, 10, 12, 14]),
303 (9, 16, &[0, 2, 3, 5, 7, 9, 10, 12, 14]),
304 ];
305
306 const BARS: [(usize, &[usize]); 10] = [
308 (2, &[0, 8]),
309 (3, &[0, 5, 10]),
310 (4, &[0, 4, 8, 12]),
311 (5, &[0, 3, 6, 9, 12]),
312 (6, &[0, 3, 5, 8, 11, 13]),
313 (7, &[0, 3, 5, 7, 10, 12, 14]),
314 (8, &[0, 2, 4, 6, 8, 10, 12, 14]),
315 (9, &[0, 2, 3, 5, 7, 9, 10, 12, 14]),
316 (10, &[0, 2, 3, 5, 7, 8, 10, 11, 13, 15]),
317 (11, &[0, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15]),
318 ];
319
320 fn floor_form(k: usize, n: usize) -> Vec<bool> {
321 (0..n).map(|i| (i * k) % n < k).collect()
322 }
323
324 #[test]
325 fn reproduces_every_printed_pattern() {
326 for (k, n, steps) in PRINTED {
327 let pattern = euclid(k, n);
328 assert_eq!(pattern.len(), n);
329 assert_eq!(onsets(&pattern), steps, "E({k},{n})");
330 }
331 }
332
333 #[test]
334 fn reproduces_the_bar_table() {
335 for (k, steps) in BARS {
336 assert_eq!(onsets(&euclid(k, 16)), steps, "E({k},16)");
337 }
338 assert_eq!(onsets(&euclid(4, 16)), [0, 4, 8, 12]);
339 }
340
341 #[test]
342 fn the_two_lists_hold_27_distinct_patterns() {
343 let mut all: Vec<(usize, usize)> = PRINTED.iter().map(|(k, n, _)| (*k, *n)).collect();
344 all.extend(BARS.iter().map(|(k, _)| (*k, 16)));
345 all.sort_unstable();
346 all.dedup();
347 assert_eq!(all.len(), 27);
348 }
349
350 #[test]
351 fn the_floor_form_fails_exactly_nine_printed_patterns() {
352 let failing: Vec<(usize, usize)> = PRINTED
353 .iter()
354 .filter(|(k, n, _)| floor_form(*k, *n) != euclid(*k, *n))
355 .map(|(k, n, _)| (*k, *n))
356 .collect();
357 assert_eq!(
358 failing,
359 [
360 (2, 5),
361 (3, 7),
362 (4, 9),
363 (5, 8),
364 (5, 11),
365 (5, 13),
366 (7, 12),
367 (5, 16),
368 (9, 16)
369 ]
370 );
371 }
372
373 #[test]
374 fn rotation_moves_onsets_by_r_modulo_n() {
375 for (k, n, _) in PRINTED {
376 let pattern = euclid(k, n);
377 for r in 0..n {
378 let turned: Vec<usize> = onsets(&rotate(&pattern, r));
379 let mut expected: Vec<usize> =
380 onsets(&pattern).iter().map(|s| (s + r) % n).collect();
381 expected.sort_unstable();
382 assert_eq!(turned, expected, "E({k},{n}) by {r}");
383 }
384 }
385 assert_eq!(euclid(0, 4), [false; 4]);
386 assert_eq!(euclid(4, 4), [true; 4]);
387 }
388
389 #[test]
390 fn swing_matches_the_published_table() {
391 let rows = [
392 (54.0, 1.17, 0.08, 9.6, [13.3, 10.0, 8.6]),
393 (58.0, 1.38, 0.16, 19.2, [26.7, 20.0, 17.1]),
394 (62.0, 1.63, 0.24, 28.8, [40.0, 30.0, 25.7]),
395 (66.7, 2.00, 0.33, 40.0, [55.6, 41.7, 35.7]),
396 (75.0, 3.00, 0.50, 60.0, [83.3, 62.5, 53.6]),
397 ];
398 for (pct, ratio, delay, ticks, ms) in rows {
399 assert!((swing_ratio(pct).unwrap() - ratio).abs() < 0.006, "{pct}");
400 assert!((swing_delay(pct).unwrap() - delay).abs() < 0.006, "{pct}");
401 assert!(
402 (swing_ticks(pct, Pair::Sixteenths).unwrap() - ticks).abs() < 0.1,
403 "{pct}"
404 );
405 for (bpm, late) in [90.0, 120.0, 140.0].into_iter().zip(ms) {
406 let computed = swing_ticks(pct, Pair::Sixteenths).unwrap() * tick_ms(bpm).unwrap();
407 assert!((computed - late).abs() < 0.1, "{pct} at {bpm}: {computed}");
408 }
409 assert!((swing_pct(swing_ratio(pct).unwrap()).unwrap() - pct).abs() < 1e-9);
410 }
411 assert_eq!(swing_delay(50.0), Some(0.0));
412 assert!((swing_ticks(66.7, Pair::Eighths).unwrap() - 80.16).abs() < 0.01);
413 assert_eq!(
414 [Pair::Sixteenths.ticks(), Pair::Eighths.ticks()],
415 [240, 480]
416 );
417 }
418
419 #[test]
420 fn swing_refuses_a_negative_or_infinite_result() {
421 for pct in [49.9, 0.0, -10.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
422 assert_eq!(swing_delay(pct), None, "{pct}");
423 assert_eq!(swing_ticks(pct, Pair::Sixteenths), None, "{pct}");
424 assert_eq!(swing_ticks(pct, Pair::Eighths), None, "{pct}");
425 }
426 for pct in [100.0, 150.0, -1.0, f64::NAN, f64::INFINITY] {
427 assert_eq!(swing_ratio(pct), None, "{pct}");
428 }
429 assert_eq!(swing_ratio(0.0), Some(0.0));
430 assert_eq!(swing_ratio(75.0), Some(3.0));
431 for ratio in [-1.0, -2.0, -0.5, f64::NAN, f64::INFINITY] {
432 assert_eq!(swing_pct(ratio), None, "{ratio}");
433 }
434 assert_eq!(swing_pct(0.0), Some(0.0));
435 assert_eq!(swing_pct(3.0), Some(75.0));
436 assert_eq!(swing_pct(1e300), Some(100.0));
437 assert_eq!(swing_pct(1e307), Some(100.0));
438 assert_eq!(swing_pct(f64::MAX), Some(100.0));
439 for zero in [swing_ratio(-0.0), swing_pct(-0.0)] {
440 assert!(zero.is_some_and(f64::is_sign_positive), "{zero:?}");
441 }
442 }
443
444 #[test]
445 fn tempo_lengths_refuse_a_tempo_outside_the_bpm_range() {
446 for bpm in [0.0, -120.0, f64::NAN, f64::INFINITY, 3.9, 1000.5] {
447 assert_eq!(tick_ms(bpm), None, "{bpm}");
448 assert_eq!(sixteenth_ms(bpm), None, "{bpm}");
449 }
450 assert_eq!(tick_ms(120.0), Some(1000.0 / 960.0));
451 assert_eq!(sixteenth_ms(120.0), Some(125.0));
452 for bpm in [4.0, 1000.0] {
453 assert!(tick_ms(bpm).is_some(), "{bpm}");
454 assert!(sixteenth_ms(bpm).is_some(), "{bpm}");
455 }
456 assert_eq!(tick_ms(4.0), Some(31.25));
457 assert_eq!(sixteenth_ms(1000.0), Some(15.0));
458 }
459
460 #[test]
464 fn tempo_lengths_match_the_published_formulas_to_two_ulp() {
465 let ulps = |a: f64, b: f64| a.to_bits().abs_diff(b.to_bits());
466 for hundredths in 400_u32..=100_000 {
467 let bpm = f64::from(hundredths) / 100.0;
468 let tick = 60_000.0 / (bpm * f64::from(TICKS_PER_BEAT));
469 assert!(ulps(tick_ms(bpm).unwrap(), tick) <= 2, "{bpm}");
470 assert!(
471 ulps(sixteenth_ms(bpm).unwrap(), 60_000.0 / bpm / 4.0) <= 2,
472 "{bpm}"
473 );
474 }
475 }
476
477 #[test]
478 fn ticks_for_ms_refuses_a_bad_tempo_or_length() {
479 for bpm in [0.0, -120.0, f64::NAN, f64::INFINITY, 3.9, 1000.5] {
480 assert_eq!(ticks_for_ms(5.0, bpm), None, "{bpm}");
481 }
482 for ms in [f64::NAN, -1.0, f64::INFINITY, 1e12] {
483 assert_eq!(ticks_for_ms(ms, 120.0), None, "{ms}");
484 }
485 assert_eq!(ticks_for_ms(0.0, 120.0), Some(0));
486 assert_eq!(ticks_for_ms(-0.0, 120.0), Some(0));
487 assert_eq!(ticks_for_ms(1000.0, 120.0), Some(960));
488 assert_eq!(ticks_for_ms(1000.0, 4.0), Some(32));
489 }
490
491 #[test]
492 fn slot_ticks_refuses_a_grid_that_does_not_split_the_bar() {
493 assert_eq!(slot_ticks(0), None);
494 assert_eq!(slot_ticks(7), None);
495 assert_eq!(slot_ticks(1), Some(1920));
496 assert_eq!(slot_ticks(32), Some(60));
497 }
498
499 #[test]
500 fn ticks_and_milliseconds_match_the_published_values() {
501 let ticks: Vec<u32> = Division::ALL
502 .iter()
503 .map(|division| division.ticks())
504 .collect();
505 assert_eq!(ticks, [60, 80, 120, 160, 240, 320, 360, 480, 720, 1920]);
506 for division in Division::ALL {
507 let beats = division.division().beats_per_cycle();
508 assert_eq!(
509 beats * f64::from(TICKS_PER_BEAT),
510 f64::from(division.ticks()),
511 "{}",
512 division.name()
513 );
514 assert!(audio::Division::ALL.contains(&division.division()));
515 }
516 for (bpm, ms) in [(120.0, 1.042), (140.0, 0.893), (174.0, 0.718)] {
517 assert!((tick_ms(bpm).unwrap() - ms).abs() < 0.001, "{bpm}");
518 }
519 for (bpm, ms) in [(90.0, 166.7), (120.0, 125.0), (140.0, 107.1), (174.0, 86.2)] {
520 assert!((sixteenth_ms(bpm).unwrap() - ms).abs() < 0.05, "{bpm}");
521 }
522 assert_eq!(ticks_for_ms(5.0, 120.0), Some(5));
523 assert_eq!(ticks_for_ms(5.0, 174.0), Some(7));
524 assert_eq!(
525 [slot_ticks(12), slot_ticks(16), slot_ticks(24)],
526 [Some(160), Some(120), Some(80)]
527 );
528 assert!([160, 120, 80].iter().all(|ticks| ticks % 20 == 0));
529 }
530}