Skip to main content

rust_synth/math/
rhythm.rs

1//! Euclidean rhythm generator (Bjorklund-style) + step-grid helpers.
2//!
3//! Distributes `hits` events as evenly as possible across `steps` and
4//! rotates by `rotation`. Encodes the result as a `u32` bitmask where
5//! bit `i` is 1 if step `i` is an active hit. 16-step resolution (4 per
6//! beat, 4 beats per bar) is plenty for drum machines.
7
8pub const STEPS: u32 = 16;
9
10/// Packed 16-step pattern as a u32 bitmask. Bit 0 = step 0.
11pub fn euclidean_bits(hits: u32, rotation: u32) -> u32 {
12    let steps = STEPS;
13    let hits = hits.min(steps);
14    if hits == 0 {
15        return 0;
16    }
17    // Equidistribution: step i is active when floor(i·hits/steps) !=
18    // floor((i-1)·hits/steps). This is a fast approximation of
19    // Bjorklund's algorithm — identical output for divisor pairs
20    // (e.g. 16/4, 16/8) and musically indistinguishable elsewhere.
21    let mut bits = 0u32;
22    for i in 0..hits {
23        let idx = (i * steps) / hits;
24        let rotated = (idx + rotation) % steps;
25        bits |= 1 << rotated;
26    }
27    bits
28}
29
30/// Returns (global_step_index, phase_within_step) given time in seconds.
31#[inline]
32pub fn step_position(t: f64, bpm: f64, steps_per_beat: f64) -> (u64, f64) {
33    let pos = t * bpm / 60.0 * steps_per_beat;
34    (pos as u64, pos.fract())
35}
36
37/// Check if the pattern has a hit at `(t * bpm / 60 * 4) mod 16`.
38#[inline]
39pub fn step_is_active(bits: u32, t: f64, bpm: f64) -> (bool, f64) {
40    let (idx, phi) = step_position(t, bpm, 4.0);
41    let step = (idx % STEPS as u64) as u32;
42    let active = (bits >> step) & 1 == 1;
43    (active, phi)
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    #[test]
51    fn four_on_the_floor() {
52        let bits = euclidean_bits(4, 0);
53        // 4 hits on 16 steps, rotation 0 → positions 0, 4, 8, 12
54        assert_eq!(bits, 0b0001_0001_0001_0001);
55    }
56
57    #[test]
58    fn empty_pattern_when_no_hits() {
59        assert_eq!(euclidean_bits(0, 0), 0);
60    }
61
62    #[test]
63    fn rotation_shifts() {
64        let base = euclidean_bits(4, 0);
65        let rotated = euclidean_bits(4, 2);
66        // Should be base shifted left by 2 (wrapping in 16 bits).
67        let expected = ((base << 2) | (base >> 14)) & 0xFFFF;
68        assert_eq!(rotated, expected);
69    }
70
71    #[test]
72    fn hits_count_matches() {
73        for h in 0..=16 {
74            let bits = euclidean_bits(h, 0);
75            assert_eq!(bits.count_ones(), h);
76        }
77    }
78}