1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
//! Interlocking rhythms, Euclidean rhythms, and metric modulation.
/// Greatest common divisor (Euclidean algorithm).
pub fn gcd(a: u32, b: u32) -> u32 {
if b == 0 { a } else { gcd(b, a % b) }
}
/// Least common multiple.
pub fn lcm(a: u32, b: u32) -> u32 {
if a == 0 || b == 0 {
0
} else {
a / gcd(a, b) * b
}
}
/// Bjorklund / Euclidean rhythm: distribute `pulses` onsets as evenly as
/// possible across `steps` slots.
///
/// Uses the Bresenham-inspired rotation algorithm:
/// - Start with `pulses` groups of [true] and `(steps - pulses)` groups of [false].
/// - Repeatedly fold the shorter list into the longer until one list has length ≤ 1.
pub fn euclidean_rhythm(pulses: u32, steps: u32) -> Vec<bool> {
if steps == 0 {
return vec![];
}
if pulses == 0 {
return vec![false; steps as usize];
}
let pulses = pulses.min(steps);
// Start with `pulses` groups of [true] and `remainder` groups of [false]
let mut ones: Vec<Vec<bool>> = (0..pulses).map(|_| vec![true]).collect();
let mut zeros: Vec<Vec<bool>> = (0..(steps - pulses)).map(|_| vec![false]).collect();
loop {
if zeros.len() <= 1 {
break;
}
// Zip the shorter into the longer
let (shorter, longer) = if ones.len() <= zeros.len() {
(&mut ones, &mut zeros)
} else {
(&mut zeros, &mut ones)
};
let take = shorter.len().min(longer.len());
let drain: Vec<Vec<bool>> = longer.drain(longer.len() - take..).collect();
for (s, d) in shorter.iter_mut().zip(drain.into_iter()) {
s.extend(d);
}
if ones.len() == zeros.len() {
break;
}
}
ones.into_iter().chain(zeros).flatten().collect()
}
/// One layer in a polyrhythm.
pub struct PolyrhythmLayer {
/// Human-readable name.
pub name: String,
/// Boolean pulse pattern (length = number of steps in one cycle).
pub pulse_pattern: Vec<bool>,
/// Tempo for this layer in beats per minute.
pub bpm: f64,
/// Swing factor (0.0 = none, 1.0 = full swing).
pub swing: f64,
/// How many steps this layer is offset from the downbeat.
pub offset_steps: u32,
}
impl PolyrhythmLayer {
/// Compute the timestamp (in seconds) of each true beat for `n_bars` repetitions.
pub fn beat_times_sec(&self, n_bars: u32) -> Vec<f64> {
if self.pulse_pattern.is_empty() || self.bpm <= 0.0 {
return vec![];
}
let steps = self.pulse_pattern.len() as u32;
let step_dur = 60.0 / self.bpm; // seconds per step
let total_steps = steps * n_bars;
let offset = self.offset_steps as usize % self.pulse_pattern.len().max(1);
let mut times = Vec::new();
for i in 0..total_steps as usize {
let pat_idx = (i + offset) % self.pulse_pattern.len();
if self.pulse_pattern[pat_idx] {
let mut t = i as f64 * step_dur;
// Apply swing on even steps: lengthen odd subdivisions
if self.swing > 0.0 && (i % 2 == 1) {
t += step_dur * self.swing * 0.5;
}
times.push(t);
}
}
times
}
}
/// Engine combining multiple polyrhythm layers.
pub struct PolyrhythmEngine {
/// Individual rhythm layers.
pub layers: Vec<PolyrhythmLayer>,
/// Global master tempo in BPM.
pub master_bpm: f64,
}
impl PolyrhythmEngine {
/// Create a new engine with the given master tempo.
pub fn new(master_bpm: f64) -> Self {
Self {
layers: Vec::new(),
master_bpm,
}
}
/// Add a layer to the engine.
pub fn add_layer(&mut self, layer: PolyrhythmLayer) {
self.layers.push(layer);
}
/// LCM of all layer step counts. 1 if no layers.
pub fn period_steps(&self) -> u32 {
if self.layers.is_empty() {
return 1;
}
self.layers
.iter()
.map(|l| l.pulse_pattern.len() as u32)
.fold(1u32, lcm)
}
/// `[step][layer]` grid of beats for `n_bars` of the period.
pub fn collision_grid(&self, n_bars: u32) -> Vec<Vec<bool>> {
if self.layers.is_empty() {
return vec![];
}
let period = self.period_steps() as usize;
let total_steps = period * n_bars as usize;
let mut grid: Vec<Vec<bool>> = vec![vec![false; self.layers.len()]; total_steps];
for (li, layer) in self.layers.iter().enumerate() {
let pat_len = layer.pulse_pattern.len();
if pat_len == 0 {
continue;
}
let offset = layer.offset_steps as usize % pat_len;
for step in 0..total_steps {
let pat_idx = (step + offset) % pat_len;
grid[step][li] = layer.pulse_pattern[pat_idx];
}
}
grid
}
/// Return timestamps (seconds) where 2 or more layers coincide.
pub fn coincidence_points(&self, n_bars: u32) -> Vec<f64> {
if self.master_bpm <= 0.0 {
return vec![];
}
let grid = self.collision_grid(n_bars);
let step_dur = 60.0 / self.master_bpm;
grid.iter()
.enumerate()
.filter(|(_, beats)| beats.iter().filter(|&&b| b).count() >= 2)
.map(|(i, _)| i as f64 * step_dur)
.collect()
}
/// Render an ASCII grid: one line per layer, 'X' = beat, '.' = rest.
pub fn render_ascii(&self) -> String {
if self.layers.is_empty() {
return String::new();
}
let period = self.period_steps() as usize;
let mut out = String::new();
for layer in &self.layers {
let pat_len = layer.pulse_pattern.len();
let label = format!("{:<12} |", layer.name);
out.push_str(&label);
for step in 0..period {
let pat_idx = if pat_len > 0 { step % pat_len } else { 0 };
let ch = if pat_len > 0 && layer.pulse_pattern[pat_idx] {
'X'
} else {
'.'
};
out.push(ch);
}
out.push('\n');
}
out
}
}
/// Type of metric modulation.
pub enum ModulationType {
/// Instant tempo change.
Abrupt,
/// Linearly interpolated tempo change.
GradualLinear,
/// Pivot on a shared note value.
PivotNote,
}
impl ModulationType {
/// Return the effective BPM at `beat` (0-based) within the modulation window.
pub fn new_bpm_at_beat(&self, beat: u32, mm: &MetricModulation) -> f64 {
let total = mm.duration_beats.max(1) as f64;
let t = (beat as f64 / total).clamp(0.0, 1.0);
match self {
ModulationType::Abrupt => {
if beat == 0 {
mm.from_bpm
} else {
mm.to_bpm
}
}
ModulationType::GradualLinear => mm.from_bpm + (mm.to_bpm - mm.from_bpm) * t,
ModulationType::PivotNote => {
// Pivot: stays at from_bpm until midpoint, then snaps to to_bpm
if t < 0.5 {
mm.from_bpm
} else {
mm.to_bpm
}
}
}
}
}
/// A metric modulation: transition from one tempo to another over a span.
pub struct MetricModulation {
/// Starting tempo.
pub from_bpm: f64,
/// Target tempo.
pub to_bpm: f64,
/// Duration of the transition in beats.
pub duration_beats: u32,
/// Modulation curve type.
pub modulation_type: ModulationType,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn euclidean_examples() {
// E(3,8) = [1,0,0,1,0,0,1,0]
let r = euclidean_rhythm(3, 8);
assert_eq!(r.len(), 8);
assert_eq!(r.iter().filter(|&&b| b).count(), 3);
// E(4,4) = all beats
let r = euclidean_rhythm(4, 4);
assert!(r.iter().all(|&b| b));
// E(0,4) = silence
let r = euclidean_rhythm(0, 4);
assert!(r.iter().all(|&b| !b));
}
#[test]
fn lcm_gcd() {
assert_eq!(gcd(12, 8), 4);
assert_eq!(lcm(3, 4), 12);
assert_eq!(lcm(6, 4), 12);
}
#[test]
fn engine_period_and_coincidence() {
let mut engine = PolyrhythmEngine::new(120.0);
engine.add_layer(PolyrhythmLayer {
name: "3-beat".to_string(),
pulse_pattern: euclidean_rhythm(3, 3),
bpm: 120.0,
swing: 0.0,
offset_steps: 0,
});
engine.add_layer(PolyrhythmLayer {
name: "4-beat".to_string(),
pulse_pattern: euclidean_rhythm(4, 4),
bpm: 120.0,
swing: 0.0,
offset_steps: 0,
});
assert_eq!(engine.period_steps(), 12); // lcm(3,4)
let pts = engine.coincidence_points(1);
assert!(!pts.is_empty()); // they coincide at step 0 at least
}
#[test]
fn metric_modulation_gradual() {
let mm = MetricModulation {
from_bpm: 100.0,
to_bpm: 200.0,
duration_beats: 10,
modulation_type: ModulationType::GradualLinear,
};
let mid = mm.modulation_type.new_bpm_at_beat(5, &mm);
assert!((mid - 150.0).abs() < 1.0);
}
}