1#[inline]
9pub fn beat_phase(t: f64, bpm: f64) -> f64 {
10 let period = 60.0 / bpm.max(1.0);
11 t.rem_euclid(period) / period
12}
13
14#[inline]
15pub fn pulse_decay(t: f64, bpm: f64, decay: f64) -> f64 {
16 (-beat_phase(t, bpm) * decay).exp()
17}
18
19#[inline]
20pub fn pulse_sine(t: f64, bpm: f64) -> f64 {
21 0.5 - 0.5 * (std::f64::consts::TAU * beat_phase(t, bpm)).cos()
22}
23
24#[inline]
25pub fn phrase_phase(t: f64, bpm: f64, beats: f64) -> f64 {
26 let period = beats * 60.0 / bpm.max(1.0);
27 t.rem_euclid(period) / period
28}
29
30#[inline]
41pub fn arp_offset_semitones(t: f64, bpm: f64, depth: f64, seed: u64) -> f64 {
42 let d = depth.clamp(0.0, 1.0);
43 if d < 1.0e-4 {
44 return 0.0;
45 }
46 let beats_per_step = 2.0;
47 let step = (t * bpm.max(1.0) / 60.0 / beats_per_step) as u64;
48 const SCALE: [f64; 5] = [0.0, 2.0, 4.0, 7.0, 9.0];
50 let mut h = seed ^ step.wrapping_mul(0x9E37_79B9_7F4A_7C15);
52 h ^= h >> 30;
53 h = h.wrapping_mul(0xBF58_476D_1CE4_E5B9);
54 h ^= h >> 27;
55 let idx = (h >> 32) as usize % SCALE.len();
56 SCALE[idx] * d
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn beat_phase_at_zero() {
65 assert!(beat_phase(0.0, 120.0).abs() < 1e-12);
66 }
67
68 #[test]
69 fn pulse_decay_is_one_on_beat() {
70 let v = pulse_decay(0.0, 90.0, 8.0);
71 assert!((v - 1.0).abs() < 1e-12);
72 }
73
74 #[test]
75 fn pulse_decay_falls_within_beat() {
76 let beat = 60.0 / 90.0;
77 let start = pulse_decay(0.0, 90.0, 8.0);
78 let later = pulse_decay(beat * 0.5, 90.0, 8.0);
79 assert!(later < start);
80 }
81
82 #[test]
83 fn phase_stable_at_hour() {
84 let t = 3600.0;
87 let p = beat_phase(t, 72.0);
88 assert!((0.0..1.0).contains(&p));
89 }
90}