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