Skip to main content

music21_rs/
polyrhythm.rs

1use num::integer::{gcd, lcm};
2use std::collections::BTreeMap;
3
4use crate::chord::Chord;
5use crate::defaults::{FloatType, IntegerType, UnsignedIntegerType};
6use crate::duration::Duration;
7use crate::error::{Error, Result};
8use crate::interval::Interval;
9use crate::meter::TimeSignature;
10use crate::note::Note;
11use crate::pitch::Pitch;
12use crate::stream::{Stream, StreamKind};
13use crate::tempo::MetronomeMark;
14
15#[derive(Debug, Clone)]
16/// A repeating polyrhythm defined by a base meter and subdivision voices.
17#[must_use]
18pub struct Polyrhythm {
19    /// Beats per measure (e.g. 4 for 4/4 time)
20    pub base: UnsignedIntegerType,
21    /// Subdivisions (e.g. [3, 4] for a 3:4 polyrhythm)
22    pub components: Vec<UnsignedIntegerType>,
23    /// Tempo in BPM. `None` means no tempo has been assigned yet.
24    pub tempo: Option<UnsignedIntegerType>,
25    /// Total ticks per measure (lcm of subdivisions)
26    pub cycle: UnsignedIntegerType,
27    current_tick: UnsignedIntegerType,
28}
29
30#[derive(Debug, Clone, PartialEq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32/// A single tick in a polyrhythm cycle.
33#[must_use]
34pub struct PolyrhythmEvent {
35    /// Tick index within the cycle.
36    pub tick: UnsignedIntegerType,
37    /// Time in seconds from the start of the cycle.
38    pub time_seconds: FloatType,
39    /// Per-component trigger flags for this tick.
40    pub triggers: Vec<bool>,
41}
42
43#[derive(Debug, Clone, PartialEq)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45/// A chord tone inferred from a polyrhythm's subdivision ratios.
46#[must_use]
47pub struct PolyrhythmRatioTone {
48    /// The reduced subdivision component that produced this tone.
49    pub component: UnsignedIntegerType,
50    /// The nearest whole number of semitones above the lowest reduced ratio.
51    pub offset: IntegerType,
52    /// Frequency ratio above the lowest reduced ratio.
53    pub ratio: FloatType,
54    /// The exact distance above the lowest reduced ratio, in cents.
55    pub cents: FloatType,
56}
57
58#[derive(Debug, Clone, PartialEq)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60/// Timing and ratio analysis for one polyrhythm cycle.
61#[must_use]
62pub struct PolyrhythmAnalysis {
63    /// Beats per measure.
64    pub base: UnsignedIntegerType,
65    /// Subdivision voices.
66    pub components: Vec<UnsignedIntegerType>,
67    /// Tempo in beats per minute.
68    pub tempo: UnsignedIntegerType,
69    /// Total ticks per measure.
70    pub cycle: UnsignedIntegerType,
71    /// Duration of one tick in seconds.
72    pub tick_duration: FloatType,
73    /// Tick interval for each subdivision voice.
74    pub component_intervals: Vec<UnsignedIntegerType>,
75    /// Tick events where at least one voice triggers.
76    pub hit_events: Vec<PolyrhythmEvent>,
77    /// Ratio-derived chord tones.
78    pub ratio_tones: Vec<PolyrhythmRatioTone>,
79}
80
81impl Polyrhythm {
82    /// Creates a polyrhythm from a base meter and nonzero subdivisions.
83    pub fn new(base: UnsignedIntegerType, subdivisions: &[UnsignedIntegerType]) -> Result<Self> {
84        if base == 0 {
85            return Err(Error::Polyrhythm("Base must be nonzero".into()));
86        }
87        if subdivisions.is_empty() {
88            return Err(Error::Polyrhythm(
89                "At least one subdivision is required".into(),
90            ));
91        }
92        for &sub in subdivisions {
93            if sub == 0 {
94                return Err(Error::Polyrhythm("Subdivision must be nonzero".into()));
95            }
96        }
97        let cycle = subdivisions.iter().fold(1, |acc, &x| lcm(acc, x));
98        Ok(Self {
99            base,
100            components: subdivisions.to_vec(),
101            tempo: None,
102            cycle,
103            current_tick: 0,
104        })
105    }
106
107    /// Creates a polyrhythm from a time-signature numerator, tempo, and
108    /// subdivision voices.
109    pub fn from_time_signature(
110        beats_per_measure: UnsignedIntegerType,
111        tempo: UnsignedIntegerType,
112        subdivisions: &[UnsignedIntegerType],
113    ) -> Result<Self> {
114        Self::new(beats_per_measure, subdivisions)?.with_tempo(tempo)
115    }
116
117    /// Returns this polyrhythm with a nonzero tempo in beats per minute.
118    pub fn with_tempo(mut self, tempo: UnsignedIntegerType) -> Result<Self> {
119        self.set_tempo(tempo)?;
120        Ok(self)
121    }
122
123    /// Sets the tempo in beats per minute.
124    pub fn set_tempo(&mut self, tempo: UnsignedIntegerType) -> Result<()> {
125        if tempo == 0 {
126            return Err(Error::Polyrhythm("Tempo must be nonzero".into()));
127        }
128        self.tempo = Some(tempo);
129        Ok(())
130    }
131
132    /// Returns the tempo in beats per minute.
133    ///
134    /// Returns `None` when the polyrhythm was constructed without a tempo and
135    /// [`Self::set_tempo`] has not been called.
136    pub fn tempo(&self) -> Option<UnsignedIntegerType> {
137        self.tempo
138    }
139
140    /// Returns the subdivision voices.
141    pub fn components(&self) -> &[UnsignedIntegerType] {
142        &self.components
143    }
144
145    /// Returns the current iterator tick.
146    pub fn current_tick(&self) -> UnsignedIntegerType {
147        self.current_tick
148    }
149
150    /// Resets iteration to the first tick in the cycle.
151    pub fn reset(&mut self) {
152        self.current_tick = 0;
153    }
154
155    /// Returns the tick interval for each subdivision voice.
156    pub fn component_intervals(&self) -> Vec<UnsignedIntegerType> {
157        self.components
158            .iter()
159            .map(|sub| self.cycle / *sub)
160            .collect()
161    }
162
163    /// Returns the duration of one measure (in seconds)
164    pub fn measure_duration(&self) -> Result<FloatType> {
165        match self.tempo {
166            Some(tempo) => Ok(self.base as FloatType * 60.0 / (tempo as FloatType)),
167            None => Err(Error::Polyrhythm("Tempo not set".into())),
168        }
169    }
170
171    /// Returns the duration of one tick (smallest subdivision unit) in seconds.
172    pub fn tick_duration(&self) -> Result<FloatType> {
173        Ok(self.measure_duration()? / self.cycle as FloatType)
174    }
175
176    /// Returns the number of ticks in one full cycle.
177    pub fn cycle_len(&self) -> UnsignedIntegerType {
178        self.cycle
179    }
180
181    /// Returns beat timings (in seconds) for each subdivision voice over one
182    /// full measure.
183    pub fn beat_timings(&self) -> Result<Vec<Vec<FloatType>>> {
184        let tick_duration = self.tick_duration()?;
185        Ok(self
186            .components
187            .iter()
188            .map(|&sub| {
189                let interval = self.cycle / sub;
190                (0..sub)
191                    .map(|i| (i * interval) as FloatType * tick_duration)
192                    .collect()
193            })
194            .collect())
195    }
196
197    /// Returns all tick events in one full cycle.
198    pub fn events(&self) -> Result<Vec<PolyrhythmEvent>> {
199        let tick_duration = self.tick_duration()?;
200        Ok((0..self.cycle)
201            .map(|tick| {
202                let triggers = self
203                    .components
204                    .iter()
205                    .map(|&sub| {
206                        let divisor = self.cycle / sub;
207                        divisor != 0 && tick % divisor == 0
208                    })
209                    .collect::<Vec<_>>();
210                PolyrhythmEvent {
211                    tick,
212                    time_seconds: tick as FloatType * tick_duration,
213                    triggers,
214                }
215            })
216            .collect())
217    }
218
219    /// Returns only events where at least one component triggers.
220    pub fn hit_events(&self) -> Result<Vec<PolyrhythmEvent>> {
221        Ok(self
222            .events()?
223            .into_iter()
224            .filter(|event| event.triggers.iter().any(|trigger| *trigger))
225            .collect())
226    }
227
228    /// Returns ratio-derived chord tones for the subdivision components.
229    ///
230    /// Components are first reduced by their greatest common divisor, and the
231    /// smallest is the root: a component sounds `component / root` above it,
232    /// which is `cents` exactly and `offset` to the nearest semitone. Two
233    /// components that round to the same semitone give one tone, the smaller.
234    pub fn ratio_tones(&self) -> Vec<PolyrhythmRatioTone> {
235        let divisor = self
236            .components
237            .iter()
238            .copied()
239            .reduce(gcd)
240            .unwrap_or(1)
241            .max(1);
242        let reduced_components = self
243            .components
244            .iter()
245            .map(|component| component / divisor)
246            .collect::<Vec<_>>();
247        let root_ratio = reduced_components.iter().copied().min().unwrap_or(1).max(1);
248        let mut tones_by_offset = BTreeMap::new();
249
250        for component in reduced_components {
251            let ratio = component as FloatType / root_ratio as FloatType;
252            let offset = (12.0 * ratio.log2()).round() as IntegerType;
253            tones_by_offset.entry(offset).or_insert(component);
254        }
255
256        tones_by_offset
257            .into_iter()
258            .map(|(offset, component)| {
259                let ratio = component as FloatType / root_ratio as FloatType;
260                PolyrhythmRatioTone {
261                    component,
262                    offset,
263                    ratio,
264                    cents: 1200.0 * ratio.log2(),
265                }
266            })
267            .collect()
268    }
269
270    /// Returns the pitches the subdivision ratios sound above `base`, in tune.
271    ///
272    /// Each tone is spelled at its nearest semitone and carries the rest as a
273    /// microtone, so a 4:5:6 rhythm on `C4` is `C4 E4(-14c) G4(+2c)`: the just
274    /// major triad, not the equal-tempered one.
275    pub fn ratio_pitches<T>(&self, base: T) -> Result<Vec<Pitch>>
276    where
277        T: TryInto<Pitch>,
278        T::Error: Into<Error>,
279    {
280        let base_pitch = base.try_into().map_err(Into::into)?;
281        self.ratio_tones()
282            .into_iter()
283            .map(|tone| pitch_above(&base_pitch, tone.cents))
284            .collect()
285    }
286
287    /// One measure of the rhythm as a score: a part for each subdivision, in
288    /// the order given, each playing its ratio's pitch above `base` as
289    /// [`Polyrhythm::ratio_pitches`] tunes it, so the score is heard as the
290    /// rhythm and its chord at once.
291    ///
292    /// The measure is `base` quarter notes in `base/4`, and a subdivision of
293    /// `n` divides it into `n` equal notes, which are tuplets wherever `n`
294    /// does not divide it evenly. The first part carries the tempo, when one
295    /// is set.
296    ///
297    /// ```
298    /// use music21_rs::Polyrhythm;
299    ///
300    /// let score = Polyrhythm::new(4, &[3, 2])?.with_tempo(90)?.to_score("C4")?;
301    /// let parts = score.parts();
302    /// assert_eq!(parts.len(), 2);
303    /// let triplets = parts[0].measures()[0].notes();
304    /// assert_eq!(triplets.len(), 3);
305    /// assert!((triplets[1].0 - 4.0 / 3.0).abs() < 1e-9);
306    /// # Ok::<(), music21_rs::Error>(())
307    /// ```
308    pub fn to_score<T>(&self, base: T) -> Result<Stream>
309    where
310        T: TryInto<Pitch>,
311        T::Error: Into<Error>,
312    {
313        let base_pitch = base.try_into().map_err(Into::into)?;
314        let divisor = self
315            .components
316            .iter()
317            .copied()
318            .reduce(gcd)
319            .unwrap_or(1)
320            .max(1);
321        let root = self.components.iter().copied().min().unwrap_or(1).max(1) / divisor;
322        let bar = FloatType::from(self.base);
323        let mut score = Stream::with_kind(StreamKind::Score);
324        for (index, &component) in self.components.iter().enumerate() {
325            let ratio = FloatType::from(component / divisor) / FloatType::from(root);
326            let pitch = pitch_above(&base_pitch, 1200.0 * ratio.log2())?;
327            let length = bar / FloatType::from(component);
328            let mut measure = Stream::with_kind(StreamKind::Measure);
329            measure.set_number(1);
330            measure.insert(0.0, TimeSignature::new(self.base, 4)?);
331            if let (0, Some(tempo)) = (index, self.tempo) {
332                measure.insert(0.0, MetronomeMark::new(FloatType::from(tempo)));
333            }
334            for beat in 0..component {
335                let note = Note::from_pitch(pitch.clone()).with_duration(Duration::new(length)?);
336                measure.insert(FloatType::from(beat) * length, note);
337            }
338            let mut part = Stream::with_kind(StreamKind::Part);
339            part.set_name(Some(component.to_string()));
340            part.push(measure);
341            score.push(part);
342        }
343        Ok(score)
344    }
345
346    /// Returns the frequencies the subdivision ratios sound at, the lowest at
347    /// `base_hertz`.
348    ///
349    /// This is the same ratio heard as pitch rather than as rhythm: a 3:2
350    /// polyrhythm at 110 and 165 Hz is a fifth.
351    pub fn ratio_frequencies(&self, base_hertz: FloatType) -> Vec<FloatType> {
352        self.ratio_tones()
353            .into_iter()
354            .map(|tone| base_hertz * tone.ratio)
355            .collect()
356    }
357
358    /// Converts subdivision ratios into a chord above `base`.
359    pub fn ratio_chord<T>(&self, base: T) -> Result<Chord>
360    where
361        T: TryInto<Pitch>,
362        T::Error: Into<Error>,
363    {
364        let pitches = self.ratio_pitches(base)?;
365        Chord::new(pitches.as_slice())
366    }
367
368    /// Returns timing and ratio analysis for one cycle.
369    pub fn analysis(&self) -> Result<PolyrhythmAnalysis> {
370        let tempo = self
371            .tempo
372            .ok_or_else(|| Error::Polyrhythm("Tempo not set".into()))?;
373        Ok(PolyrhythmAnalysis {
374            base: self.base,
375            components: self.components.clone(),
376            tempo,
377            cycle: self.cycle,
378            tick_duration: self.tick_duration()?,
379            component_intervals: self.component_intervals(),
380            hit_events: self.hit_events()?,
381            ratio_tones: self.ratio_tones(),
382        })
383    }
384
385    /// Returns ticks where at least `min_simultaneous` components trigger.
386    pub fn coincidence_ticks(&self, min_simultaneous: usize) -> Vec<UnsignedIntegerType> {
387        if min_simultaneous == 0 {
388            return (0..self.cycle).collect();
389        }
390
391        (0..self.cycle)
392            .filter(|tick| {
393                self.components
394                    .iter()
395                    .filter(|sub| {
396                        let divisor = self.cycle / **sub;
397                        divisor != 0 && *tick % divisor == 0
398                    })
399                    .count()
400                    >= min_simultaneous
401            })
402            .collect()
403    }
404}
405
406/// `cents` above `base`, spelled at the nearest semitone with the rest as a
407/// microtone on top of any `base` already carries.
408fn pitch_above(base: &Pitch, cents: FloatType) -> Result<Pitch> {
409    let semitones = (cents / 100.0).round();
410    let mut pitch = base.transpose(&Interval::from_semitones(semitones as IntegerType)?)?;
411    let carried = base.microtone().map_or(0.0, |microtone| microtone.cents());
412    pitch.set_microtone_cents(carried + cents - 100.0 * semitones)?;
413    Ok(pitch)
414}
415
416impl Iterator for Polyrhythm {
417    type Item = (UnsignedIntegerType, Vec<bool>);
418
419    /// Advances the polyrhythm by one tick.
420    /// Returns the current tick and a vector indicating which subdivision
421    /// triggers a beat.
422    fn next(&mut self) -> Option<Self::Item> {
423        let tick = self.current_tick;
424        let triggers = self
425            .components
426            .iter()
427            .map(|&sub| {
428                let divisor = self.cycle / sub;
429                tick.checked_rem(divisor) == Some(0)
430            })
431            .collect();
432        self.current_tick = (self.current_tick + 1) % self.cycle;
433        Some((tick, triggers))
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440    use crate::stream::StreamElement;
441
442    #[test]
443    fn test_from_time_signature() {
444        let poly = Polyrhythm::from_time_signature(4, 120, &[2, 3]).unwrap();
445        // For subdivisions 2 and 3, lcm is 6 ticks per measure.
446        assert_eq!(poly.cycle_len(), 6);
447        // tick_duration = (4 * 60 / 120) / 6 = (4 * 0.5) / 6 = 2 / 6 ≈ 0.3333 sec.
448        let tick_dur = poly.tick_duration().unwrap();
449        assert!((tick_dur - 0.3333).abs() < 0.01);
450    }
451
452    #[test]
453    fn test_new_rejects_zero_base() {
454        let err = Polyrhythm::new(0, &[2, 3]).unwrap_err();
455        assert!(err.to_string().contains("Base must be nonzero"));
456    }
457
458    #[test]
459    fn test_new_rejects_empty_and_zero_subdivisions() {
460        let empty = Polyrhythm::new(4, &[]).unwrap_err();
461        assert!(empty.to_string().contains("At least one subdivision"));
462
463        let zero_subdivision = Polyrhythm::new(4, &[2, 0, 3]).unwrap_err();
464        assert!(
465            zero_subdivision
466                .to_string()
467                .contains("Subdivision must be nonzero")
468        );
469    }
470
471    #[test]
472    fn test_set_tempo_rejects_zero() {
473        let mut poly = Polyrhythm::new(4, &[2, 3]).unwrap();
474        let err = poly.set_tempo(0).unwrap_err();
475        assert!(err.to_string().contains("Tempo must be nonzero"));
476    }
477
478    #[test]
479    fn test_with_tempo_sets_tempo() {
480        let poly = Polyrhythm::new(4, &[3, 4]).unwrap().with_tempo(90).unwrap();
481        assert_eq!(poly.tempo(), Some(90));
482    }
483
484    #[test]
485    fn test_without_tempo_rejects_time_queries() {
486        let poly = Polyrhythm::new(4, &[2, 3]).unwrap();
487        assert!(poly.measure_duration().is_err());
488        assert!(poly.tick_duration().is_err());
489        assert!(poly.beat_timings().is_err());
490        assert!(poly.events().is_err());
491    }
492
493    #[test]
494    fn test_beat_timings_are_spaced_by_component_interval() {
495        let poly = Polyrhythm::from_time_signature(4, 120, &[2, 3]).unwrap();
496        let timings = poly.beat_timings().unwrap();
497        assert_eq!(timings.len(), 2);
498        assert_eq!(timings[0].len(), 2);
499        assert_eq!(timings[1].len(), 3);
500        assert!((timings[0][1] - 1.0).abs() < 0.001);
501        assert!((timings[1][1] - 0.6666).abs() < 0.01);
502    }
503
504    #[test]
505    fn test_events() {
506        let poly = Polyrhythm::from_time_signature(4, 120, &[2, 3]).unwrap();
507        let events = poly.events().unwrap();
508        assert_eq!(events.len(), 6);
509        assert_eq!(events[0].triggers, vec![true, true]);
510        assert_eq!(events[1].triggers, vec![false, false]);
511        assert_eq!(events[2].triggers, vec![false, true]);
512        assert_eq!(events[3].triggers, vec![true, false]);
513
514        let hits = poly.hit_events().unwrap();
515        assert_eq!(hits.len(), 4);
516        assert_eq!(
517            hits.iter().map(|event| event.tick).collect::<Vec<_>>(),
518            vec![0, 2, 3, 4]
519        );
520    }
521
522    #[test]
523    fn ratio_tones_reduce_components_and_project_to_pitches() {
524        let poly = Polyrhythm::from_time_signature(4, 120, &[3, 4, 6]).unwrap();
525        let tones = poly.ratio_tones();
526        assert_eq!(
527            tones
528                .iter()
529                .map(|tone| (tone.component, tone.offset))
530                .collect::<Vec<_>>(),
531            vec![(3, 0), (4, 5), (6, 12)]
532        );
533
534        let pitches = poly.ratio_pitches("C4").unwrap();
535        assert_eq!(
536            pitches
537                .iter()
538                .map(Pitch::name_with_octave)
539                .collect::<Vec<_>>(),
540            vec!["C4", "F4", "C5"]
541        );
542        // 4/3 is a just fourth, two cents flat of the tempered one.
543        assert!((tones[1].cents - 498.044_999_134_798).abs() < 1e-9);
544        assert!((pitches[1].microtone().unwrap().cents() + 1.955).abs() < 1e-3);
545        assert!(pitches[2].microtone().is_none());
546
547        let analysis = poly.analysis().unwrap();
548        assert_eq!(analysis.component_intervals, vec![4, 3, 2]);
549        assert_eq!(analysis.ratio_tones, tones);
550    }
551
552    #[test]
553    fn test_coincidence_ticks() {
554        let poly = Polyrhythm::from_time_signature(4, 120, &[2, 3]).unwrap();
555        assert_eq!(poly.coincidence_ticks(0), vec![0, 1, 2, 3, 4, 5]);
556        assert_eq!(poly.coincidence_ticks(2), vec![0]);
557        assert_eq!(poly.coincidence_ticks(1), vec![0, 2, 3, 4]);
558    }
559
560    #[test]
561    fn a_rhythm_of_four_five_and_six_is_a_just_major_triad() {
562        let poly = Polyrhythm::new(4, &[4, 5, 6]).unwrap();
563        let pitches = poly.ratio_pitches("C4").unwrap();
564        let written = pitches
565            .iter()
566            .map(|pitch| {
567                let microtone = pitch.microtone().map(ToString::to_string);
568                format!(
569                    "{}{}",
570                    pitch.name_with_octave(),
571                    microtone.unwrap_or_default()
572                )
573            })
574            .collect::<Vec<_>>();
575        assert_eq!(written, ["C4", "E4(-14c)", "G4(+2c)"]);
576
577        let hertz = poly.ratio_frequencies(200.0);
578        assert_eq!(hertz, [200.0, 250.0, 300.0]);
579        // The pitches sound where the ratios say.
580        let just = pitches[1].frequency_hz() / pitches[0].frequency_hz();
581        assert!((just - 1.25).abs() < 1e-3);
582    }
583
584    #[test]
585    fn a_score_holds_a_part_for_each_subdivision_in_tuplets() {
586        let poly = Polyrhythm::new(4, &[4, 5, 6])
587            .unwrap()
588            .with_tempo(72)
589            .unwrap();
590        let score = poly.to_score("C4").unwrap();
591        let parts = score.parts();
592        assert_eq!(parts.len(), 3);
593        for (part, (count, name)) in parts.iter().zip([(4, "C4"), (5, "E4"), (6, "G4")]) {
594            let notes = part.measures()[0].notes();
595            assert_eq!(notes.len(), count);
596            let StreamElement::Note(note) = &notes[0].1 else {
597                panic!("a part plays notes");
598            };
599            assert_eq!(note.pitch().name_with_octave(), name);
600            let total: FloatType = notes
601                .iter()
602                .map(|(_, element)| element.duration().map_or(0.0, |d| d.quarter_length()))
603                .sum();
604            assert!((total - 4.0).abs() < 1e-9);
605        }
606        let StreamElement::Note(quintuplet) = &parts[1].measures()[0].notes()[0].1 else {
607            panic!("a part plays notes");
608        };
609        assert!(!quintuplet.duration().unwrap().tuplets().is_empty());
610        assert_eq!(parts[1].name(), Some("5"));
611    }
612
613    #[cfg(feature = "musicxml")]
614    #[test]
615    fn a_score_writes_as_musicxml_with_its_tuplets() {
616        use crate::musicxml::{ExportOptions, to_musicxml};
617        for components in [[3, 2], [5, 4], [7, 3]] {
618            let score = Polyrhythm::new(4, &components)
619                .unwrap()
620                .with_tempo(60)
621                .unwrap()
622                .to_score("A3")
623                .unwrap();
624            let xml = to_musicxml(&score, &ExportOptions::default()).unwrap();
625            assert!(xml.contains("<time-modification>"), "{components:?}");
626            assert!(
627                xml.contains("<per-minute>60</per-minute>"),
628                "{components:?}"
629            );
630        }
631    }
632
633    #[test]
634    fn test_iterator_state_and_reset() {
635        let mut poly = Polyrhythm::new(4, &[2, 4]).unwrap();
636        assert_eq!(poly.components(), &[2, 4]);
637        assert_eq!(poly.component_intervals(), vec![2, 1]);
638        assert_eq!(poly.current_tick(), 0);
639
640        assert_eq!(poly.next(), Some((0, vec![true, true])));
641        assert_eq!(poly.current_tick(), 1);
642        assert_eq!(poly.next(), Some((1, vec![false, true])));
643        poly.reset();
644        assert_eq!(poly.current_tick(), 0);
645    }
646}