Skip to main content

music21_rs/
tempo.rs

1//! Metronome marks, a port of the stream-free part of music21's `tempo`.
2
3use crate::{
4    defaults::FloatType,
5    duration::Duration,
6    error::{Error, Result},
7};
8
9/// The tempo words music21 knows and the beats per minute each implies,
10/// in music21's order.
11pub const DEFAULT_TEMPO_VALUES: [(&str, FloatType); 30] = [
12    ("larghissimo", 16.0),
13    ("largamente", 32.0),
14    ("grave", 40.0),
15    ("molto adagio", 40.0),
16    ("largo", 46.0),
17    ("lento", 52.0),
18    ("adagio", 56.0),
19    ("slow", 56.0),
20    ("langsam", 56.0),
21    ("larghetto", 60.0),
22    ("adagietto", 66.0),
23    ("andante", 72.0),
24    ("andantino", 80.0),
25    ("andante moderato", 83.0),
26    ("maestoso", 88.0),
27    ("moderato", 92.0),
28    ("moderate", 92.0),
29    ("allegretto", 108.0),
30    ("animato", 120.0),
31    ("allegro moderato", 128.0),
32    ("allegro", 132.0),
33    ("fast", 132.0),
34    ("schnell", 132.0),
35    ("allegrissimo", 140.0),
36    ("molto allegro", 144.0),
37    ("très vite", 144.0),
38    ("vivace", 160.0),
39    ("vivacissimo", 168.0),
40    ("presto", 184.0),
41    ("prestissimo", 208.0),
42];
43
44/// Converts a tempo counted in one note value into the same tempo counted in
45/// another, both given in quarter lengths.
46///
47/// Sixty half notes a minute is a hundred and twenty quarters.
48pub fn convert_tempo_by_referent(
49    number: FloatType,
50    source_quarter_length: FloatType,
51    destination_quarter_length: FloatType,
52) -> FloatType {
53    let seconds_per_source_beat = 60.0 / number;
54    let seconds_per_quarter = seconds_per_source_beat / source_quarter_length;
55    60.0 / (seconds_per_quarter * destination_quarter_length)
56}
57
58/// Returns the tempo word music21 pairs with a beats-per-minute value, when
59/// one lies within two beats of it. Ties go to the lower value, then the
60/// alphabetically earlier word, as music21 sorts them.
61pub fn default_text_for_number(number: FloatType) -> Option<&'static str> {
62    let mut sorted = DEFAULT_TEMPO_VALUES;
63    sorted.sort_by(|left, right| {
64        left.1
65            .partial_cmp(&right.1)
66            .unwrap_or(std::cmp::Ordering::Equal)
67            .then_with(|| left.0.cmp(right.0))
68    });
69    sorted
70        .iter()
71        .find(|(_, value)| (value - 2.0..=value + 2.0).contains(&number))
72        .map(|(text, _)| *text)
73}
74
75/// Returns the beats per minute music21 pairs with a tempo word, matching the
76/// whole text case-insensitively first and any single word of it second.
77pub fn default_number_for_text(text: &str) -> Option<FloatType> {
78    let lowered = text.to_lowercase();
79    let lookup = |candidate: &str| {
80        DEFAULT_TEMPO_VALUES
81            .iter()
82            .find(|(name, _)| *name == candidate)
83            .map(|(_, value)| *value)
84    };
85    lookup(&lowered).or_else(|| lookup(text)).or_else(|| {
86        text.split(' ')
87            .filter_map(|word| lookup(&word.to_lowercase()))
88            .next_back()
89    })
90}
91
92/// A metronome marking: a beats-per-minute number, a tempo word, and the
93/// note value the number counts.
94///
95/// Either half may be implied from the other: a number alone picks up the
96/// nearest tempo word, and a word alone picks up its conventional number.
97#[derive(Clone, Debug, PartialEq)]
98#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
99#[must_use]
100pub struct MetronomeMark {
101    number: Option<FloatType>,
102    text: Option<String>,
103    referent: Duration,
104    number_implicit: bool,
105    text_implicit: bool,
106    /// What the mark is played at, where that is not what it says.
107    #[cfg_attr(feature = "serde", serde(default))]
108    number_sounding: Option<FloatType>,
109}
110
111impl Default for MetronomeMark {
112    /// A mark that says nothing yet: no number, no word, counting quarters.
113    /// music21 builds one of these and fills it in.
114    fn default() -> Self {
115        Self::build(None, None, Duration::quarter())
116    }
117}
118
119impl MetronomeMark {
120    /// A mark of `number` beats per minute, counting quarter notes, with the
121    /// tempo word implied from the number when one is close enough.
122    pub fn new(number: FloatType) -> Self {
123        Self::build(Some(number), None, Duration::quarter())
124    }
125
126    /// A mark from a tempo word alone, with the number implied from the word
127    /// when music21 knows it.
128    pub fn from_text(text: impl Into<String>) -> Self {
129        Self::build(None, Some(text.into()), Duration::quarter())
130    }
131
132    /// A mark carrying both an explicit number and an explicit word.
133    pub fn with_number_and_text(number: FloatType, text: impl Into<String>) -> Self {
134        Self::build(Some(number), Some(text.into()), Duration::quarter())
135    }
136
137    /// Changes the note value the number counts, so `Duration::half()` makes
138    /// the number a count of half notes.
139    pub fn with_referent(mut self, referent: Duration) -> Self {
140        self.referent = referent;
141        self
142    }
143
144    fn build(number: Option<FloatType>, text: Option<String>, referent: Duration) -> Self {
145        let number_implicit = number.is_none();
146        let text_implicit = text.is_none();
147        let number = number.or_else(|| text.as_deref().and_then(default_number_for_text));
148        let text = text.or_else(|| number.and_then(default_text_for_number).map(String::from));
149        Self {
150            number_implicit: number_implicit && number.is_some(),
151            text_implicit: text_implicit && text.is_some(),
152            number,
153            text,
154            referent,
155            number_sounding: None,
156        }
157    }
158
159    /// The same tempo counted in a different note value: music21's
160    /// `getEquivalentByReferent`, so quarter = 60 becomes eighth = 120. The
161    /// tempo word is carried over unchanged, implied or not.
162    pub fn equivalent_by_referent(&self, referent: Duration) -> MetronomeMark {
163        let number = self.number.map(|number| {
164            convert_tempo_by_referent(
165                number,
166                self.referent.quarter_length(),
167                referent.quarter_length(),
168            )
169        });
170        Self::build(number, self.text.clone(), referent)
171    }
172
173    /// The same number counted in a different note value, so the tempo
174    /// itself changes: music21's `getMaintainedNumberWithReferent`.
175    pub fn maintained_number_with_referent(&self, referent: Duration) -> MetronomeMark {
176        Self::build(self.number, self.text.clone(), referent)
177    }
178
179    /// The beats per minute, if known.
180    pub fn number(&self) -> Option<FloatType> {
181        self.number
182    }
183
184    /// Sets the beats per minute, which is then no longer implied. A mark
185    /// with no word of its own picks one up, as music21 does.
186    pub fn set_number(&mut self, number: Option<FloatType>) {
187        self.number = number;
188        self.number_implicit = false;
189        if self.text.is_none()
190            && let Some(number) = number
191            && let Some(text) = default_text_for_number(number)
192        {
193            self.text = Some(text.to_string());
194            self.text_implicit = true;
195        }
196    }
197
198    /// Sets the tempo word, which is then no longer implied. A mark with no
199    /// number of its own picks one up.
200    pub fn set_text(&mut self, text: impl Into<String>) {
201        let text = text.into();
202        self.text_implicit = false;
203        if self.number.is_none()
204            && let Some(number) = default_number_for_text(&text)
205        {
206            self.number = Some(number);
207            self.number_implicit = true;
208        }
209        self.text = Some(text);
210    }
211
212    /// Sets the note value the number counts, leaving the number alone — so
213    /// the tempo itself changes.
214    pub fn set_referent(&mut self, referent: Duration) {
215        self.referent = referent;
216    }
217
218    /// The tempo word, if any.
219    pub fn text(&self) -> Option<&str> {
220        self.text.as_deref()
221    }
222
223    /// The tempo word as something a score would carry: music21's
224    /// `getTextExpression`. A word only implied from the number is not
225    /// answered unless `return_implicit` asks for it.
226    pub fn text_expression(&self, return_implicit: bool) -> Option<&str> {
227        if self.text_implicit && !return_implicit {
228            return None;
229        }
230        self.text.as_deref()
231    }
232
233    /// The note value the number counts.
234    pub fn referent(&self) -> &Duration {
235        &self.referent
236    }
237
238    /// Whether the number was implied from the tempo word.
239    pub fn number_implicit(&self) -> bool {
240        self.number_implicit
241    }
242
243    /// Says so, or unsays it. music21 lets a caller write this: its MIDI
244    /// reader copies a mark onto every staff and marks the copies implicit,
245    /// which is how the parts after the first hide the number.
246    pub fn set_number_implicit(&mut self, implicit: bool) {
247        self.number_implicit = implicit;
248    }
249
250    /// Whether the tempo word was implied from the number.
251    pub fn text_implicit(&self) -> bool {
252        self.text_implicit
253    }
254
255    /// music21's `numberSounding`: how fast the mark is actually played,
256    /// where that is not the number written over the staff.
257    ///
258    /// A score may say *Allegro* and be played at a hundred and forty-four,
259    /// and a MusicXML `<sound tempo=...>` with no metronome mark beside it
260    /// is a tempo that sounds and is never written. Nothing here is
261    /// answered from it unless it is asked for: [`Self::quarter_bpm`] reads
262    /// the written number, and [`Self::sounding_quarter_bpm`] reads this.
263    pub fn number_sounding(&self) -> Option<FloatType> {
264        self.number_sounding
265    }
266
267    /// Sets how fast the mark is played, apart from what it says.
268    pub fn set_number_sounding(&mut self, number: Option<FloatType>) {
269        self.number_sounding = number;
270    }
271
272    /// A mark built to be played at this speed.
273    ///
274    /// It does not become the mark's number: a mark that says nothing and
275    /// sounds at a hundred and sixty-eight still says nothing, which is how
276    /// music21 carries a tempo that is played and never written.
277    pub fn with_number_sounding(mut self, number: FloatType) -> Self {
278        self.number_sounding = Some(number);
279        self
280    }
281
282    /// The tempo as quarter notes per minute, whatever the referent.
283    pub fn quarter_bpm(&self) -> Option<FloatType> {
284        self.number
285            .map(|number| convert_tempo_by_referent(number, self.referent.quarter_length(), 1.0))
286    }
287
288    /// The same, at the speed the mark is played rather than the one it
289    /// says: music21's `getQuarterBPM(useNumberSounding=True)`.
290    pub fn sounding_quarter_bpm(&self) -> Option<FloatType> {
291        match self.number_sounding {
292            Some(number) => Some(convert_tempo_by_referent(
293                number,
294                self.referent.quarter_length(),
295                1.0,
296            )),
297            None => self.quarter_bpm(),
298        }
299    }
300
301    /// Seconds each quarter note lasts.
302    ///
303    /// A mark of nought beats a minute is refused rather than answered with
304    /// an infinity, which is what dividing by it gives and what every
305    /// arithmetic downstream of it would then carry.
306    pub fn seconds_per_quarter(&self) -> Result<FloatType> {
307        let quarter_bpm = self.quarter_bpm().ok_or_else(|| {
308            Error::Tempo("cannot derive seconds without a tempo number".to_string())
309        })?;
310        if quarter_bpm == 0.0 {
311            return Err(Error::Tempo(
312                "a tempo of no beats a minute lasts no seconds a beat".to_string(),
313            ));
314        }
315        Ok(60.0 / quarter_bpm)
316    }
317
318    /// Seconds a span of the given quarter length lasts at this tempo.
319    pub fn quarter_length_to_seconds(&self, quarter_length: FloatType) -> Result<FloatType> {
320        Ok(self.seconds_per_quarter()? * quarter_length)
321    }
322
323    /// Seconds a duration lasts at this tempo.
324    pub fn duration_to_seconds(&self, duration: &Duration) -> Result<FloatType> {
325        self.quarter_length_to_seconds(duration.quarter_length())
326    }
327
328    /// The duration that lasts the given number of seconds at this tempo.
329    pub fn seconds_to_duration(&self, seconds: FloatType) -> Result<Duration> {
330        if seconds.is_nan() || seconds <= 0.0 {
331            return Err(Error::Tempo(
332                "seconds must be a number greater than zero".to_string(),
333            ));
334        }
335        Duration::new(seconds / self.seconds_per_quarter()?)
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn a_mark_can_be_rewritten_one_half_at_a_time() {
345        let mut mark = MetronomeMark::from_text("adagio");
346        assert!(mark.number_implicit());
347        mark.set_number(Some(60.0));
348        assert!(!mark.number_implicit());
349        assert_eq!(mark.number(), Some(60.0));
350        mark.set_number_implicit(true);
351        assert!(mark.number_implicit());
352        mark.set_referent(Duration::half());
353        assert_eq!(mark.referent().quarter_length(), 2.0);
354        assert_eq!(mark.quarter_bpm(), Some(120.0));
355
356        let mut numbered = MetronomeMark::new(90.0);
357        assert!(numbered.text_implicit());
358        numbered.set_text("largo");
359        assert!(!numbered.text_implicit());
360        assert_eq!(numbered.text(), Some("largo"));
361        assert_eq!(numbered.number(), Some(90.0));
362        let mut wordless = MetronomeMark::default();
363        wordless.set_number(Some(184.0));
364        assert_eq!(wordless.text(), Some("presto"));
365        assert!(wordless.text_implicit());
366        wordless.set_number(None);
367        assert_eq!(wordless.number(), None);
368    }
369
370    /// music21's own `getTextExpression` examples.
371    #[test]
372    fn an_implied_word_is_only_answered_when_asked_for() {
373        let presto = MetronomeMark::from_text("presto");
374        assert_eq!(presto.number(), Some(184.0));
375        assert_eq!(presto.text_expression(false), Some("presto"));
376
377        let ninety = MetronomeMark::new(90.0);
378        assert!(ninety.text_implicit());
379        assert_eq!(ninety.text_expression(false), None);
380        assert_eq!(ninety.text_expression(true), Some("maestoso"));
381    }
382
383    #[test]
384    fn a_mark_may_be_played_faster_than_it_is_written() {
385        // music21's own example: a tempo that sounds and is never written.
386        let playback = MetronomeMark::default().with_number_sounding(168.0);
387        assert_eq!(playback.number_sounding(), Some(168.0));
388        // It says nothing: the tempo is played and never written.
389        assert_eq!(playback.number(), None);
390        assert_eq!(playback.text(), None);
391        assert_eq!(playback.quarter_bpm(), None);
392        assert_eq!(playback.sounding_quarter_bpm(), Some(168.0));
393
394        // A mark that says a number keeps it, and only the sounding tempo
395        // reads the other.
396        let mut written = MetronomeMark::new(60.0).with_referent(Duration::half());
397        assert_eq!(written.quarter_bpm(), Some(120.0));
398        assert_eq!(written.sounding_quarter_bpm(), Some(120.0));
399        written.set_number_sounding(Some(90.0));
400        assert_eq!(written.number(), Some(60.0));
401        assert_eq!(written.quarter_bpm(), Some(120.0));
402        assert_eq!(written.sounding_quarter_bpm(), Some(180.0));
403    }
404
405    #[test]
406    fn referent_changes_match_music21() {
407        let quarter_sixty = MetronomeMark::new(60.0);
408        let eighths = quarter_sixty.equivalent_by_referent(Duration::eighth());
409        assert_eq!(eighths.number(), Some(120.0));
410        assert_eq!(eighths.referent().quarter_length(), 0.5);
411        assert_eq!(eighths.text(), Some("larghetto"));
412        let halves = quarter_sixty.equivalent_by_referent(Duration::half());
413        assert_eq!(halves.number(), Some(30.0));
414
415        let andante = MetronomeMark::with_number_and_text(72.0, "andante")
416            .with_referent(Duration::new(1.5).unwrap());
417        let quarters = andante.equivalent_by_referent(Duration::quarter());
418        assert_eq!(quarters.number(), Some(108.0));
419        assert_eq!(quarters.text(), Some("andante"));
420        assert_eq!(
421            andante.equivalent_by_referent(Duration::half()).number(),
422            Some(54.0)
423        );
424
425        let kept = andante.maintained_number_with_referent(Duration::eighth());
426        assert_eq!(kept.number(), Some(72.0));
427        assert_eq!(kept.referent().quarter_length(), 0.5);
428        assert_eq!(kept.text(), Some("andante"));
429    }
430
431    #[test]
432    fn numbers_imply_the_tempo_words_music21_picks() {
433        let cases = [
434            (120.0, Some("animato")),
435            (60.0, Some("larghetto")),
436            (61.0, Some("larghetto")),
437            (62.0, Some("larghetto")),
438            (63.0, None),
439            (90.0, Some("maestoso")),
440            (92.0, Some("moderate")),
441            (100.0, None),
442            (118.0, Some("animato")),
443            (122.0, Some("animato")),
444            (123.0, None),
445            (16.0, Some("larghissimo")),
446            (12.0, None),
447            (56.5, Some("adagio")),
448            (300.0, None),
449        ];
450        for (number, text) in cases {
451            let mark = MetronomeMark::new(number);
452            assert_eq!(mark.text(), text, "{number}");
453            assert_eq!(mark.text_implicit(), text.is_some(), "{number}");
454            assert!(!mark.number_implicit());
455        }
456    }
457
458    #[test]
459    fn words_imply_their_conventional_numbers() {
460        let cases = [
461            ("allegro", Some(132.0)),
462            ("Allegro", Some(132.0)),
463            ("ALLEGRO ", Some(132.0)),
464            ("très vite", Some(144.0)),
465            ("andante moderato", Some(83.0)),
466            ("unknown words", None),
467        ];
468        for (text, number) in cases {
469            let mark = MetronomeMark::from_text(text);
470            assert_eq!(mark.number(), number, "{text}");
471            assert_eq!(mark.number_implicit(), number.is_some(), "{text}");
472            assert_eq!(mark.text(), Some(text));
473            assert!(!mark.text_implicit());
474        }
475        let both = MetronomeMark::with_number_and_text(120.0, "fast");
476        assert_eq!(both.number(), Some(120.0));
477        assert_eq!(both.text(), Some("fast"));
478        assert!(!both.number_implicit() && !both.text_implicit());
479    }
480
481    #[test]
482    fn referents_convert_to_quarter_bpm_and_seconds() {
483        let quarter = MetronomeMark::new(120.0);
484        assert_eq!(quarter.quarter_bpm(), Some(120.0));
485        assert_eq!(quarter.quarter_length_to_seconds(1.0).unwrap(), 0.5);
486        assert_eq!(
487            quarter.seconds_to_duration(0.75).unwrap().quarter_length(),
488            1.5
489        );
490
491        let half = MetronomeMark::new(60.0).with_referent(Duration::half());
492        assert_eq!(half.quarter_bpm(), Some(120.0));
493        assert_eq!(half.text(), Some("larghetto"));
494        assert_eq!(half.quarter_length_to_seconds(1.0).unwrap(), 0.5);
495        assert_eq!(
496            half.duration_to_seconds(&Duration::new(3.0).unwrap())
497                .unwrap(),
498            1.5
499        );
500        assert_eq!(half.seconds_to_duration(1.0).unwrap().quarter_length(), 2.0);
501
502        let eighth = MetronomeMark::new(120.0).with_referent(Duration::eighth());
503        assert_eq!(eighth.quarter_bpm(), Some(60.0));
504        assert_eq!(eighth.seconds_per_quarter().unwrap(), 1.0);
505
506        let dotted = MetronomeMark::new(56.5).with_referent(Duration::half());
507        assert!((dotted.quarter_bpm().unwrap() - 113.0).abs() < 1e-9);
508
509        assert_eq!(convert_tempo_by_referent(60.0, 1.0, 2.0), 30.0);
510        assert_eq!(convert_tempo_by_referent(60.0, 2.0, 1.0), 120.0);
511
512        let unknown = MetronomeMark::from_text("unknown words");
513        assert_eq!(unknown.quarter_bpm(), None);
514        assert!(unknown.seconds_per_quarter().is_err());
515        assert!(quarter.seconds_to_duration(0.0).is_err());
516
517        let motionless = MetronomeMark::new(0.0);
518        assert_eq!(motionless.quarter_bpm(), Some(0.0));
519        assert!(motionless.seconds_per_quarter().is_err());
520        assert!(
521            motionless
522                .duration_to_seconds(&Duration::quarter())
523                .is_err()
524        );
525        assert!(motionless.quarter_length_to_seconds(1.0).is_err());
526    }
527}