Skip to main content

music21_rs/
meter.rs

1//! Time signatures, ported from music21's `meter` package.
2//!
3//! [`TimeSignature`] covers the part of music21's meter handling that is a pure
4//! function of the numerator and denominator: how long a bar is, how many beats
5//! it carries, how long each beat is, and how that beat subdivides.
6//!
7//! music21 derives all of this from a `MeterSequence` partition tree that also
8//! drives beaming, display sequences and accent weighting. Only the partition
9//! *result* is ported here — the tree's other consumers have no counterpart in
10//! this crate yet, and building the tree to read one number back off it would be
11//! the transliterated machinery the repository guidance warns against. The
12//! partition rule itself is music21's `_setDefaultBeatPartitions`, verified
13//! against upstream by the `meter_parity` fixture.
14
15use crate::defaults::{FloatType, UnsignedIntegerType};
16use crate::duration::Duration;
17use crate::error::{Error, Result};
18
19/// Names music21 gives a partition count, indexed by the count itself.
20///
21/// Index 0 is music21's `Empty`, which a valid time signature never reaches.
22const BEAT_COUNT_NAMES: [&str; 9] = [
23    "Empty",
24    "Single",
25    "Duple",
26    "Triple",
27    "Quadruple",
28    "Quintuple",
29    "Sextuple",
30    "Septuple",
31    "Octuple",
32];
33
34/// How the beat of a meter subdivides.
35///
36/// Mirrors music21's `beatDivisionCountName`.
37#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub enum BeatDivision {
40    /// A single-beat meter, which music21 reports as `Other` rather than as a
41    /// division — there is no lower level to divide.
42    Other,
43    /// Beats divide in two.
44    Simple,
45    /// Beats divide in three.
46    Compound,
47}
48
49impl BeatDivision {
50    /// Returns music21's name for this division.
51    pub fn music21_name(self) -> &'static str {
52        match self {
53            Self::Other => "Other",
54            Self::Simple => "Simple",
55            Self::Compound => "Compound",
56        }
57    }
58
59    /// Returns the number of divisions in one beat.
60    ///
61    /// Matches music21's `beatDivisionCount`, which reports `1` rather than
62    /// raising for a single-beat meter.
63    pub fn count(self) -> UnsignedIntegerType {
64        match self {
65            Self::Other => 1,
66            Self::Simple => 2,
67            Self::Compound => 3,
68        }
69    }
70}
71
72/// A time signature, such as `4/4` or `6/8`.
73///
74/// ```
75/// use music21_rs::TimeSignature;
76///
77/// let six_eight = TimeSignature::from_ratio_string("6/8")?;
78/// assert_eq!(six_eight.beat_count(), 2);
79/// assert_eq!(six_eight.beat_quarter_length(), 1.5);
80/// assert_eq!(six_eight.classification(), "Compound Duple");
81/// # Ok::<(), music21_rs::Error>(())
82/// ```
83#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85pub struct TimeSignature {
86    numerator: UnsignedIntegerType,
87    denominator: UnsignedIntegerType,
88}
89
90impl Default for TimeSignature {
91    /// Returns `4/4`, matching music21's default `TimeSignature()`.
92    fn default() -> Self {
93        Self::common()
94    }
95}
96
97impl TimeSignature {
98    /// Creates a time signature from a numerator and denominator.
99    ///
100    /// Both must be non-zero. The denominator need not be a power of two —
101    /// music21 accepts irrational meters such as `4/3`, and so does this.
102    pub fn new(numerator: UnsignedIntegerType, denominator: UnsignedIntegerType) -> Result<Self> {
103        if numerator == 0 {
104            return Err(Error::Meter(
105                "time signature numerator must be non-zero".to_string(),
106            ));
107        }
108        if denominator == 0 {
109            return Err(Error::Meter(
110                "time signature denominator must be non-zero".to_string(),
111            ));
112        }
113        Ok(Self {
114            numerator,
115            denominator,
116        })
117    }
118
119    /// Parses a `"numerator/denominator"` string such as `"6/8"`.
120    pub fn from_ratio_string(ratio: &str) -> Result<Self> {
121        let (numerator, denominator) = ratio.split_once('/').ok_or_else(|| {
122            Error::Meter(format!(
123                "time signature {ratio:?} is not `numerator/denominator`"
124            ))
125        })?;
126        let parse = |part: &str, label: &str| {
127            part.trim().parse::<UnsignedIntegerType>().map_err(|_| {
128                Error::Meter(format!("cannot read a {label} from {part:?} in {ratio:?}"))
129            })
130        };
131        Self::new(
132            parse(numerator, "numerator")?,
133            parse(denominator, "denominator")?,
134        )
135    }
136
137    /// Returns common time, `4/4`.
138    pub fn common() -> Self {
139        Self {
140            numerator: 4,
141            denominator: 4,
142        }
143    }
144
145    /// Returns cut time, `2/2`.
146    pub fn cut() -> Self {
147        Self {
148            numerator: 2,
149            denominator: 2,
150        }
151    }
152
153    /// Returns the numerator.
154    pub fn numerator(self) -> UnsignedIntegerType {
155        self.numerator
156    }
157
158    /// Returns the denominator.
159    pub fn denominator(self) -> UnsignedIntegerType {
160        self.denominator
161    }
162
163    /// Returns the `"numerator/denominator"` spelling.
164    pub fn ratio_string(self) -> String {
165        format!("{}/{}", self.numerator, self.denominator)
166    }
167
168    /// Returns the length of one bar in quarter lengths.
169    pub fn bar_quarter_length(self) -> FloatType {
170        FloatType::from(self.numerator) * 4.0 / FloatType::from(self.denominator)
171    }
172
173    /// Returns the length of one bar as a [`Duration`].
174    pub fn bar_duration(self) -> Duration {
175        Duration::new(self.bar_quarter_length())
176            .expect("a non-zero numerator and denominator give a positive finite bar length")
177    }
178
179    /// Returns how many beats one bar carries.
180    ///
181    /// This is music21's `beatCount`, which follows the numerator rather than
182    /// the denominator — except at `3`, where `3/4` is three beats but `3/8` is
183    /// one.
184    pub fn beat_count(self) -> UnsignedIntegerType {
185        match self.numerator {
186            1 => 1,
187            2 => 2,
188            // music21 treats 3 as a single beat once the denominator is short
189            // enough that the bar reads as one compound unit.
190            3 if self.denominator > 4 => 1,
191            3 => 3,
192            4 => 4,
193            6 => 2,
194            9 => 3,
195            12 => 4,
196            numerator if numerator >= 15 && numerator.is_multiple_of(3) => numerator / 3,
197            numerator => numerator,
198        }
199    }
200
201    /// Returns music21's name for the beat count, such as `"Duple"`.
202    ///
203    /// Counts above eight are spelled as `"<n>-uple"`, as music21 does.
204    pub fn beat_count_name(self) -> String {
205        let count = self.beat_count();
206        BEAT_COUNT_NAMES
207            .get(count as usize)
208            .map_or_else(|| format!("{count}-uple"), |name| (*name).to_string())
209    }
210
211    /// Returns the length of one beat in quarter lengths.
212    ///
213    /// Every meter this type can express has a uniform beat, so unlike
214    /// music21's `beatDuration` this never fails. music21 only reports a
215    /// non-uniform beat for a hand-partitioned `MeterSequence`, which has no
216    /// counterpart here.
217    pub fn beat_quarter_length(self) -> FloatType {
218        self.bar_quarter_length() / FloatType::from(self.beat_count())
219    }
220
221    /// Returns the length of one beat as a [`Duration`].
222    pub fn beat_duration(self) -> Duration {
223        Duration::new(self.beat_quarter_length())
224            .expect("a positive bar length divided by a positive beat count stays positive")
225    }
226
227    /// Returns how the beat subdivides.
228    pub fn beat_division(self) -> BeatDivision {
229        if self.beat_count() == 1 {
230            BeatDivision::Other
231        } else if matches!(self.numerator, 6 | 9 | 12)
232            || (self.numerator >= 15 && self.numerator.is_multiple_of(3))
233        {
234            BeatDivision::Compound
235        } else {
236            BeatDivision::Simple
237        }
238    }
239
240    /// Returns the number of divisions in one beat.
241    pub fn beat_division_count(self) -> UnsignedIntegerType {
242        self.beat_division().count()
243    }
244
245    /// Returns `true` when beats divide in three.
246    pub fn is_compound(self) -> bool {
247        self.beat_division() == BeatDivision::Compound
248    }
249
250    /// Returns music21's `classification`, such as `"Compound Duple"`.
251    pub fn classification(self) -> String {
252        format!(
253            "{} {}",
254            self.beat_division().music21_name(),
255            self.beat_count_name()
256        )
257    }
258
259    /// Returns the quarter-length offset of each beat within one bar.
260    pub fn beat_offsets(self) -> Vec<FloatType> {
261        let beat = self.beat_quarter_length();
262        (0..self.beat_count())
263            .map(|index| FloatType::from(index) * beat)
264            .collect()
265    }
266
267    /// Returns the one-based beat containing `offset` quarter lengths into a bar.
268    ///
269    /// Matches music21's `getBeat`. Offsets at or beyond the end of the bar are
270    /// rejected rather than wrapping.
271    pub fn beat_at_offset(self, offset: FloatType) -> Result<UnsignedIntegerType> {
272        if !offset.is_finite() || offset < 0.0 || offset >= self.bar_quarter_length() {
273            return Err(Error::Meter(format!(
274                "offset {offset} is outside a {} bar of {} quarter lengths",
275                self.ratio_string(),
276                self.bar_quarter_length()
277            )));
278        }
279        let beat = (offset / self.beat_quarter_length()).floor();
280        Ok(beat as UnsignedIntegerType + 1)
281    }
282}
283
284impl std::fmt::Display for TimeSignature {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        f.write_str(&self.ratio_string())
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    fn ts(ratio: &str) -> TimeSignature {
295        TimeSignature::from_ratio_string(ratio).expect("valid time signature")
296    }
297
298    #[test]
299    fn common_and_cut_time_match_their_ratios() {
300        assert_eq!(TimeSignature::common().ratio_string(), "4/4");
301        assert_eq!(TimeSignature::cut().ratio_string(), "2/2");
302        assert_eq!(TimeSignature::default(), TimeSignature::common());
303    }
304
305    #[test]
306    fn bar_and_beat_lengths_follow_the_ratio() {
307        assert_eq!(ts("4/4").bar_quarter_length(), 4.0);
308        assert_eq!(ts("5/16").bar_quarter_length(), 1.25);
309        assert_eq!(ts("3/8").bar_quarter_length(), 1.5);
310        assert_eq!(ts("6/8").beat_quarter_length(), 1.5);
311        assert_eq!(ts("4/4").beat_quarter_length(), 1.0);
312        assert_eq!(ts("2/2").beat_duration().quarter_length(), 2.0);
313    }
314
315    #[test]
316    fn compound_meters_beat_in_threes() {
317        for (ratio, beats, division) in [
318            ("6/8", 2, BeatDivision::Compound),
319            ("9/8", 3, BeatDivision::Compound),
320            ("12/8", 4, BeatDivision::Compound),
321            ("15/8", 5, BeatDivision::Compound),
322            ("18/8", 6, BeatDivision::Compound),
323            ("24/8", 8, BeatDivision::Compound),
324        ] {
325            assert_eq!(ts(ratio).beat_count(), beats, "{ratio}");
326            assert_eq!(ts(ratio).beat_division(), division, "{ratio}");
327            assert!(ts(ratio).is_compound(), "{ratio}");
328        }
329    }
330
331    #[test]
332    fn three_is_the_one_denominator_sensitive_numerator() {
333        // 3/2 and 3/4 read as three beats; 3/8 and shorter read as one.
334        assert_eq!(ts("3/2").beat_count(), 3);
335        assert_eq!(ts("3/4").beat_count(), 3);
336        assert_eq!(ts("3/8").beat_count(), 1);
337        assert_eq!(ts("3/16").beat_count(), 1);
338        assert_eq!(ts("3/32").beat_count(), 1);
339        // Every other numerator ignores the denominator entirely.
340        for denominator in [2, 4, 8, 16] {
341            assert_eq!(TimeSignature::new(6, denominator).unwrap().beat_count(), 2);
342            assert_eq!(TimeSignature::new(5, denominator).unwrap().beat_count(), 5);
343        }
344    }
345
346    #[test]
347    fn classification_joins_division_and_count() {
348        assert_eq!(ts("4/4").classification(), "Simple Quadruple");
349        assert_eq!(ts("6/8").classification(), "Compound Duple");
350        assert_eq!(ts("3/8").classification(), "Other Single");
351        assert_eq!(ts("5/4").classification(), "Simple Quintuple");
352        assert_eq!(ts("13/8").classification(), "Simple 13-uple");
353        assert_eq!(ts("21/16").classification(), "Compound Septuple");
354    }
355
356    #[test]
357    fn beat_offsets_partition_the_bar() {
358        assert_eq!(ts("4/4").beat_offsets(), [0.0, 1.0, 2.0, 3.0]);
359        assert_eq!(ts("6/8").beat_offsets(), [0.0, 1.5]);
360        assert_eq!(ts("5/8").beat_offsets(), [0.0, 0.5, 1.0, 1.5, 2.0]);
361    }
362
363    #[test]
364    fn beat_at_offset_is_one_based_and_bounded() {
365        assert_eq!(ts("4/4").beat_at_offset(1.5).unwrap(), 2);
366        assert_eq!(ts("6/8").beat_at_offset(1.5).unwrap(), 2);
367        assert_eq!(ts("5/8").beat_at_offset(1.5).unwrap(), 4);
368        assert_eq!(ts("4/4").beat_at_offset(0.0).unwrap(), 1);
369        assert!(ts("4/4").beat_at_offset(4.0).is_err());
370        assert!(ts("4/4").beat_at_offset(-0.5).is_err());
371        assert!(ts("4/4").beat_at_offset(FloatType::NAN).is_err());
372    }
373
374    #[test]
375    fn irrational_denominators_are_accepted_as_music21_accepts_them() {
376        let four_three = ts("4/3");
377        assert!((four_three.bar_quarter_length() - 16.0 / 3.0).abs() < 1e-12);
378        assert_eq!(four_three.beat_count(), 4);
379    }
380
381    #[test]
382    fn malformed_ratios_error_instead_of_panicking() {
383        for ratio in [
384            "", "4", "4/", "/4", "4/4/4", "x/4", "4/x", "0/4", "4/0", "-1/4",
385        ] {
386            assert!(
387                TimeSignature::from_ratio_string(ratio).is_err(),
388                "{ratio:?} should not parse"
389            );
390        }
391    }
392
393    #[test]
394    fn display_is_the_ratio_string() {
395        assert_eq!(ts("7/8").to_string(), "7/8");
396    }
397}