Skip to main content

music21_rs/
polyrhythm.rs

1use num::integer::{gcd, lcm};
2use std::collections::{BTreeMap, BTreeSet};
3
4use crate::chord::Chord;
5use crate::defaults::{FloatType, IntegerType, UnsignedIntegerType};
6use crate::error::{Error, Result};
7use crate::interval::{Interval, IntervalArgument};
8use crate::pitch::Pitch;
9
10#[derive(Debug, Clone)]
11/// A repeating polyrhythm defined by a base meter and subdivision voices.
12pub struct Polyrhythm {
13    /// Beats per measure (e.g. 4 for 4/4 time)
14    pub base: UnsignedIntegerType,
15    /// Subdivisions (e.g. [3, 4] for a 3:4 polyrhythm)
16    pub components: Vec<UnsignedIntegerType>,
17    /// Tempo in BPM. `None` means no tempo has been assigned yet.
18    pub tempo: Option<UnsignedIntegerType>,
19    /// Total ticks per measure (lcm of subdivisions)
20    pub cycle: UnsignedIntegerType,
21    current_tick: UnsignedIntegerType,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26/// A single tick in a polyrhythm cycle.
27pub struct PolyrhythmEvent {
28    /// Tick index within the cycle.
29    pub tick: UnsignedIntegerType,
30    /// Time in seconds from the start of the cycle.
31    pub time_seconds: FloatType,
32    /// Per-component trigger flags for this tick.
33    pub triggers: Vec<bool>,
34}
35
36#[derive(Debug, Clone, PartialEq)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38/// A chord tone inferred from a polyrhythm's subdivision ratios.
39pub struct PolyrhythmRatioTone {
40    /// The reduced subdivision component that produced this tone.
41    pub component: UnsignedIntegerType,
42    /// Semitone offset above the lowest reduced ratio.
43    pub offset: IntegerType,
44    /// Frequency ratio above the lowest reduced ratio.
45    pub ratio: FloatType,
46}
47
48#[derive(Debug, Clone, PartialEq)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
50/// Timing and ratio analysis for one polyrhythm cycle.
51pub struct PolyrhythmAnalysis {
52    /// Beats per measure.
53    pub base: UnsignedIntegerType,
54    /// Subdivision voices.
55    pub components: Vec<UnsignedIntegerType>,
56    /// Tempo in beats per minute.
57    pub tempo: UnsignedIntegerType,
58    /// Total ticks per measure.
59    pub cycle: UnsignedIntegerType,
60    /// Duration of one tick in seconds.
61    pub tick_duration: FloatType,
62    /// Tick interval for each subdivision voice.
63    pub component_intervals: Vec<UnsignedIntegerType>,
64    /// Tick events where at least one voice triggers.
65    pub hit_events: Vec<PolyrhythmEvent>,
66    /// Ratio-derived chord tones.
67    pub ratio_tones: Vec<PolyrhythmRatioTone>,
68}
69
70impl Polyrhythm {
71    /// Creates a polyrhythm from a base meter and nonzero subdivisions.
72    pub fn new(base: UnsignedIntegerType, subdivisions: &[UnsignedIntegerType]) -> Result<Self> {
73        if base == 0 {
74            return Err(Error::Polyrhythm("Base must be nonzero".into()));
75        }
76        if subdivisions.is_empty() {
77            return Err(Error::Polyrhythm(
78                "At least one subdivision is required".into(),
79            ));
80        }
81        for &sub in subdivisions {
82            if sub == 0 {
83                return Err(Error::Polyrhythm("Subdivision must be nonzero".into()));
84            }
85        }
86        let cycle = subdivisions.iter().fold(1, |acc, &x| lcm(acc, x));
87        Ok(Self {
88            base,
89            components: subdivisions.to_vec(),
90            tempo: None,
91            cycle,
92            current_tick: 0,
93        })
94    }
95
96    /// Creates a polyrhythm from a time-signature numerator, tempo, and
97    /// subdivision voices.
98    pub fn from_time_signature(
99        beats_per_measure: UnsignedIntegerType,
100        tempo: UnsignedIntegerType,
101        subdivisions: &[UnsignedIntegerType],
102    ) -> Result<Self> {
103        Self::new(beats_per_measure, subdivisions)?.with_tempo(tempo)
104    }
105
106    /// Returns this polyrhythm with a nonzero tempo in beats per minute.
107    pub fn with_tempo(mut self, tempo: UnsignedIntegerType) -> Result<Self> {
108        self.set_tempo(tempo)?;
109        Ok(self)
110    }
111
112    /// Sets the tempo in beats per minute.
113    pub fn set_tempo(&mut self, tempo: UnsignedIntegerType) -> Result<()> {
114        if tempo == 0 {
115            return Err(Error::Polyrhythm("Tempo must be nonzero".into()));
116        }
117        self.tempo = Some(tempo);
118        Ok(())
119    }
120
121    /// Returns the tempo in beats per minute.
122    ///
123    /// Returns `None` when the polyrhythm was constructed without a tempo and
124    /// [`Self::set_tempo`] has not been called.
125    pub fn tempo(&self) -> Option<UnsignedIntegerType> {
126        self.tempo
127    }
128
129    /// Returns the subdivision voices.
130    pub fn components(&self) -> &[UnsignedIntegerType] {
131        &self.components
132    }
133
134    /// Returns the current iterator tick.
135    pub fn current_tick(&self) -> UnsignedIntegerType {
136        self.current_tick
137    }
138
139    /// Resets iteration to the first tick in the cycle.
140    pub fn reset(&mut self) {
141        self.current_tick = 0;
142    }
143
144    /// Returns the tick interval for each subdivision voice.
145    pub fn component_intervals(&self) -> Vec<UnsignedIntegerType> {
146        self.components
147            .iter()
148            .map(|sub| self.cycle / *sub)
149            .collect()
150    }
151
152    /// Returns the duration of one measure (in seconds)
153    pub fn measure_duration(&self) -> Result<FloatType> {
154        match self.tempo {
155            Some(tempo) => Ok(self.base as FloatType * 60.0 / (tempo as FloatType)),
156            None => Err(Error::Polyrhythm("Tempo not set".into())),
157        }
158    }
159
160    /// Returns the duration of one tick (smallest subdivision unit) in seconds.
161    pub fn tick_duration(&self) -> Result<FloatType> {
162        Ok(self.measure_duration()? / self.cycle as FloatType)
163    }
164
165    /// Returns the number of ticks in one full cycle.
166    pub fn cycle_len(&self) -> UnsignedIntegerType {
167        self.cycle
168    }
169
170    /// Returns beat timings (in seconds) for each subdivision voice over one
171    /// full measure.
172    pub fn beat_timings(&self) -> Result<Vec<Vec<FloatType>>> {
173        let tick_duration = self.tick_duration()?;
174        Ok(self
175            .components
176            .iter()
177            .map(|&sub| {
178                let interval = self.cycle / sub;
179                (0..sub)
180                    .map(|i| (i * interval) as FloatType * tick_duration)
181                    .collect()
182            })
183            .collect())
184    }
185
186    /// Returns all tick events in one full cycle.
187    pub fn events(&self) -> Result<Vec<PolyrhythmEvent>> {
188        let tick_duration = self.tick_duration()?;
189        Ok((0..self.cycle)
190            .map(|tick| {
191                let triggers = self
192                    .components
193                    .iter()
194                    .map(|&sub| {
195                        let divisor = self.cycle / sub;
196                        divisor != 0 && tick % divisor == 0
197                    })
198                    .collect::<Vec<_>>();
199                PolyrhythmEvent {
200                    tick,
201                    time_seconds: tick as FloatType * tick_duration,
202                    triggers,
203                }
204            })
205            .collect())
206    }
207
208    /// Returns only events where at least one component triggers.
209    pub fn hit_events(&self) -> Result<Vec<PolyrhythmEvent>> {
210        Ok(self
211            .events()?
212            .into_iter()
213            .filter(|event| event.triggers.iter().any(|trigger| *trigger))
214            .collect())
215    }
216
217    /// Returns ratio-derived chord tones for the subdivision components.
218    ///
219    /// Components are first reduced by their greatest common divisor. The
220    /// smallest reduced component is treated as the root ratio, and each
221    /// remaining component is mapped to the nearest twelve-tone semitone
222    /// offset using `12 * log2(component / root)`.
223    pub fn ratio_tones(&self) -> Vec<PolyrhythmRatioTone> {
224        let divisor = self
225            .components
226            .iter()
227            .copied()
228            .reduce(gcd)
229            .unwrap_or(1)
230            .max(1);
231        let reduced_components = self
232            .components
233            .iter()
234            .map(|component| component / divisor)
235            .collect::<Vec<_>>();
236        let root_ratio = reduced_components.iter().copied().min().unwrap_or(1).max(1);
237        let mut tones_by_offset = BTreeMap::new();
238
239        for component in reduced_components {
240            let ratio = component as FloatType / root_ratio as FloatType;
241            let offset = (12.0 * ratio.log2()).round() as IntegerType;
242            tones_by_offset.entry(offset).or_insert(component);
243        }
244
245        tones_by_offset
246            .into_iter()
247            .map(|(offset, component)| PolyrhythmRatioTone {
248                component,
249                offset,
250                ratio: component as FloatType / root_ratio as FloatType,
251            })
252            .collect()
253    }
254
255    /// Returns ratio-derived pitches above `base`.
256    pub fn ratio_pitches<T>(&self, base: T) -> Result<Vec<Pitch>>
257    where
258        T: TryInto<Pitch>,
259        T::Error: Into<Error>,
260    {
261        let base_pitch = base.try_into().map_err(Into::into)?;
262        self.ratio_tones()
263            .into_iter()
264            .map(|tone| {
265                let interval = Interval::new(IntervalArgument::Int(tone.offset))?;
266                Ok(base_pitch.transpose(interval))
267            })
268            .collect()
269    }
270
271    /// Converts subdivision ratios into a chord above `base`.
272    pub fn ratio_chord<T>(&self, base: T) -> Result<Chord>
273    where
274        T: TryInto<Pitch>,
275        T::Error: Into<Error>,
276    {
277        let pitches = self.ratio_pitches(base)?;
278        Chord::new(pitches.as_slice())
279    }
280
281    /// Returns timing and ratio analysis for one cycle.
282    pub fn analysis(&self) -> Result<PolyrhythmAnalysis> {
283        let tempo = self
284            .tempo
285            .ok_or_else(|| Error::Polyrhythm("Tempo not set".into()))?;
286        Ok(PolyrhythmAnalysis {
287            base: self.base,
288            components: self.components.clone(),
289            tempo,
290            cycle: self.cycle,
291            tick_duration: self.tick_duration()?,
292            component_intervals: self.component_intervals(),
293            hit_events: self.hit_events()?,
294            ratio_tones: self.ratio_tones(),
295        })
296    }
297
298    /// Returns ticks where at least `min_simultaneous` components trigger.
299    pub fn coincidence_ticks(&self, min_simultaneous: usize) -> Vec<UnsignedIntegerType> {
300        if min_simultaneous == 0 {
301            return (0..self.cycle).collect();
302        }
303
304        (0..self.cycle)
305            .filter(|tick| {
306                self.components
307                    .iter()
308                    .filter(|sub| {
309                        let divisor = self.cycle / **sub;
310                        divisor != 0 && *tick % divisor == 0
311                    })
312                    .count()
313                    >= min_simultaneous
314            })
315            .collect()
316    }
317
318    fn chord_from_base_pitch(&self, base_pitch: Pitch) -> Result<Chord> {
319        let mut offsets = BTreeSet::new();
320        for &sub in &self.components {
321            let interval = self.cycle / sub;
322            for i in 0..sub {
323                let tick = i * interval;
324                let ratio = tick as FloatType / self.cycle as FloatType;
325                let semitones = (ratio * 12.0).round() as IntegerType;
326                offsets.insert(semitones);
327            }
328        }
329
330        let notes: Result<Vec<Pitch>, Error> = offsets
331            .into_iter()
332            .map(|offset| {
333                let interval = Interval::new(IntervalArgument::Int(offset))?;
334                Ok(base_pitch.transpose(interval))
335            })
336            .collect();
337
338        let notes = notes?;
339        Chord::new(notes.as_slice())
340    }
341
342    /// Converts one polyrhythm cycle into a chord above `base`.
343    pub fn to_chord<T>(&self, base: T) -> Result<Chord>
344    where
345        T: TryInto<Pitch>,
346        T::Error: Into<Error>,
347    {
348        self.chord_from_base_pitch(base.try_into().map_err(Into::into)?)
349    }
350
351    /// Converts one polyrhythm cycle into a pitch collection above `base`.
352    pub fn to_polypitch<T>(&self, base: T) -> Result<Chord>
353    where
354        T: TryInto<Pitch>,
355        T::Error: Into<Error>,
356    {
357        self.to_chord(base)
358    }
359}
360
361impl Iterator for Polyrhythm {
362    type Item = (UnsignedIntegerType, Vec<bool>);
363
364    /// Advances the polyrhythm by one tick.
365    /// Returns the current tick and a vector indicating which subdivision
366    /// triggers a beat.
367    fn next(&mut self) -> Option<Self::Item> {
368        let tick = self.current_tick;
369        let triggers = self
370            .components
371            .iter()
372            .map(|&sub| {
373                let divisor = self.cycle / sub;
374                tick.checked_rem(divisor) == Some(0)
375            })
376            .collect();
377        self.current_tick = (self.current_tick + 1) % self.cycle;
378        Some((tick, triggers))
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385
386    #[test]
387    fn test_from_time_signature() {
388        let poly = Polyrhythm::from_time_signature(4, 120, &[2, 3]).unwrap();
389        // For subdivisions 2 and 3, lcm is 6 ticks per measure.
390        assert_eq!(poly.cycle_len(), 6);
391        // tick_duration = (4 * 60 / 120) / 6 = (4 * 0.5) / 6 = 2 / 6 ≈ 0.3333 sec.
392        let tick_dur = poly.tick_duration().unwrap();
393        assert!((tick_dur - 0.3333).abs() < 0.01);
394    }
395
396    #[test]
397    fn test_new_rejects_zero_base() {
398        let err = Polyrhythm::new(0, &[2, 3]).unwrap_err();
399        assert!(err.to_string().contains("Base must be nonzero"));
400    }
401
402    #[test]
403    fn test_new_rejects_empty_and_zero_subdivisions() {
404        let empty = Polyrhythm::new(4, &[]).unwrap_err();
405        assert!(empty.to_string().contains("At least one subdivision"));
406
407        let zero_subdivision = Polyrhythm::new(4, &[2, 0, 3]).unwrap_err();
408        assert!(
409            zero_subdivision
410                .to_string()
411                .contains("Subdivision must be nonzero")
412        );
413    }
414
415    #[test]
416    fn test_set_tempo_rejects_zero() {
417        let mut poly = Polyrhythm::new(4, &[2, 3]).unwrap();
418        let err = poly.set_tempo(0).unwrap_err();
419        assert!(err.to_string().contains("Tempo must be nonzero"));
420    }
421
422    #[test]
423    fn test_with_tempo_sets_tempo() {
424        let poly = Polyrhythm::new(4, &[3, 4]).unwrap().with_tempo(90).unwrap();
425        assert_eq!(poly.tempo(), Some(90));
426    }
427
428    #[test]
429    fn test_without_tempo_rejects_time_queries() {
430        let poly = Polyrhythm::new(4, &[2, 3]).unwrap();
431        assert!(poly.measure_duration().is_err());
432        assert!(poly.tick_duration().is_err());
433        assert!(poly.beat_timings().is_err());
434        assert!(poly.events().is_err());
435    }
436
437    #[test]
438    fn test_beat_timings_are_spaced_by_component_interval() {
439        let poly = Polyrhythm::from_time_signature(4, 120, &[2, 3]).unwrap();
440        let timings = poly.beat_timings().unwrap();
441        assert_eq!(timings.len(), 2);
442        assert_eq!(timings[0].len(), 2);
443        assert_eq!(timings[1].len(), 3);
444        assert!((timings[0][1] - 1.0).abs() < 0.001);
445        assert!((timings[1][1] - 0.6666).abs() < 0.01);
446    }
447
448    #[test]
449    fn test_events() {
450        let poly = Polyrhythm::from_time_signature(4, 120, &[2, 3]).unwrap();
451        let events = poly.events().unwrap();
452        assert_eq!(events.len(), 6);
453        assert_eq!(events[0].triggers, vec![true, true]);
454        assert_eq!(events[1].triggers, vec![false, false]);
455        assert_eq!(events[2].triggers, vec![false, true]);
456        assert_eq!(events[3].triggers, vec![true, false]);
457
458        let hits = poly.hit_events().unwrap();
459        assert_eq!(hits.len(), 4);
460        assert_eq!(
461            hits.iter().map(|event| event.tick).collect::<Vec<_>>(),
462            vec![0, 2, 3, 4]
463        );
464    }
465
466    #[test]
467    fn ratio_tones_reduce_components_and_project_to_pitches() {
468        let poly = Polyrhythm::from_time_signature(4, 120, &[3, 4, 6]).unwrap();
469        let tones = poly.ratio_tones();
470        assert_eq!(
471            tones
472                .iter()
473                .map(|tone| (tone.component, tone.offset))
474                .collect::<Vec<_>>(),
475            vec![(3, 0), (4, 5), (6, 12)]
476        );
477
478        let pitches = poly.ratio_pitches("C4").unwrap();
479        assert_eq!(
480            pitches
481                .iter()
482                .map(Pitch::name_with_octave)
483                .collect::<Vec<_>>(),
484            vec!["C4", "F4", "C5"]
485        );
486
487        let analysis = poly.analysis().unwrap();
488        assert_eq!(analysis.component_intervals, vec![4, 3, 2]);
489        assert_eq!(analysis.ratio_tones, tones);
490    }
491
492    #[test]
493    fn test_coincidence_ticks() {
494        let poly = Polyrhythm::from_time_signature(4, 120, &[2, 3]).unwrap();
495        assert_eq!(poly.coincidence_ticks(0), vec![0, 1, 2, 3, 4, 5]);
496        assert_eq!(poly.coincidence_ticks(2), vec![0]);
497        assert_eq!(poly.coincidence_ticks(1), vec![0, 2, 3, 4]);
498    }
499
500    #[test]
501    fn test_to_chord_is_public_and_works() {
502        let poly = Polyrhythm::from_time_signature(4, 120, &[2, 3, 4]).unwrap();
503        let chord = poly.to_chord("C4").unwrap();
504        assert!(!chord.pitched_common_name().is_empty());
505    }
506
507    #[test]
508    fn test_iterator_state_and_reset() {
509        let mut poly = Polyrhythm::new(4, &[2, 4]).unwrap();
510        assert_eq!(poly.components(), &[2, 4]);
511        assert_eq!(poly.component_intervals(), vec![2, 1]);
512        assert_eq!(poly.current_tick(), 0);
513
514        assert_eq!(poly.next(), Some((0, vec![true, true])));
515        assert_eq!(poly.current_tick(), 1);
516        assert_eq!(poly.next(), Some((1, vec![false, true])));
517        poly.reset();
518        assert_eq!(poly.current_tick(), 0);
519    }
520}