Skip to main content

music21_rs/stream/
analyze.rs

1//! Asking a stream for an analysis by name, and the two questions it
2//! answers about itself: music21's `Stream.analyze`, `isTwelveTone` and
3//! `notesAndRests`.
4
5use super::{Stream, StreamElement, StreamEvent};
6use crate::analysis::{KeyProfile, estimate_key_of_stream, stream_pitch_span};
7use crate::defaults::FloatType;
8use crate::error::{Error, Result};
9use crate::interval::Interval;
10use crate::key::Key;
11
12/// What an analysis of a stream answers, which in music21 depends on the
13/// method asked for: a key, or the span of its pitches.
14#[derive(Clone, Debug)]
15pub enum Analysis {
16    /// The key the stream is most likely in, as the key-weighting
17    /// analyses name it.
18    Key(Key),
19    /// The interval from the lowest pitch sounding to the highest:
20    /// music21's `Ambitus`.
21    Ambitus(Interval),
22}
23
24/// Which analysis a method name asks for: music21's
25/// `analysisClassFromMethodName`.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27enum Method {
28    /// The span of the pitches.
29    Ambitus,
30    /// One of the key weightings.
31    Key(KeyProfile),
32}
33
34impl Method {
35    /// The class name music21 gives it, which a method name is matched
36    /// against first.
37    fn class_name(self) -> &'static str {
38        match self {
39            Self::Ambitus => "Ambitus",
40            Self::Key(profile) => profile.music21_class_name(),
41        }
42    }
43
44    /// The name music21 calls it by, matched against next.
45    fn spoken_name(self) -> &'static str {
46        match self {
47            Self::Ambitus => AMBITUS_NAME,
48            Self::Key(profile) => profile.music21_name(),
49        }
50    }
51
52    /// The names it answers to: music21's `identifiers`.
53    fn identifiers(self) -> &'static [&'static str] {
54        match self {
55            Self::Ambitus => AMBITUS_IDENTIFIERS,
56            Self::Key(profile) => profile.identifiers(),
57        }
58    }
59}
60
61/// The name music21's `Ambitus` class calls itself by.
62const AMBITUS_NAME: &str = "Ambitus Analysis";
63
64/// The names music21's `Ambitus` answers to: its `identifiers`.
65const AMBITUS_IDENTIFIERS: &[&str] = &["ambitus", "span"];
66
67/// Every analysis a method name may ask for, in the order music21 searches
68/// them, which decides which one a loose name finds first.
69const METHODS: [Method; 6] = [
70    Method::Ambitus,
71    Method::Key(KeyProfile::KrumhanslSchmuckler),
72    Method::Key(KeyProfile::AardenEssen),
73    Method::Key(KeyProfile::SimpleWeights),
74    Method::Key(KeyProfile::BellmanBudge),
75    Method::Key(KeyProfile::TemperleyKostkaPayne),
76];
77
78/// The analysis a method name asks for: music21's
79/// `analysisClassFromMethodName`, which looks three times over, loosest
80/// last.
81fn method_named(method: &str) -> Option<Method> {
82    let lower = method.to_lowercase();
83    // Anything the class name or the spoken name holds.
84    let found = METHODS.into_iter().find(|candidate| {
85        candidate.class_name().to_lowercase().contains(&lower)
86            || candidate.spoken_name().contains(&lower)
87    });
88    if found.is_some() {
89        return found;
90    }
91    // Then a name it answers to outright.
92    let found = METHODS
93        .into_iter()
94        .find(|candidate| candidate.identifiers().contains(&method));
95    if found.is_some() {
96        return found;
97    }
98    // Then any name holding it.
99    METHODS.into_iter().find(|candidate| {
100        candidate
101            .identifiers()
102            .iter()
103            .any(|identifier| identifier.contains(method))
104    })
105}
106
107impl Stream {
108    /// The analysis a method name asks for: music21's `analyze`.
109    ///
110    /// `ambitus`, `span` and `range` give the interval from the lowest
111    /// pitch sounding to the highest; `key` and the names of music21's
112    /// five key weightings give the key the stream is most likely in. The
113    /// name need not be exact, music21 searching its own names three times
114    /// over and loosest last, so `kostka` finds Temperley's profile and a
115    /// bare `key` finds Aarden's, which is music21's default.
116    ///
117    /// ```
118    /// use music21_rs::stream::Analysis;
119    /// use music21_rs::{Note, Stream};
120    ///
121    /// let mut line = Stream::new();
122    /// for name in ["C4", "E4", "G4", "C5"] {
123    ///     line.push(Note::from_name(name)?);
124    /// }
125    /// let Some(Analysis::Ambitus(span)) = line.analyze("ambitus")? else {
126    ///     panic!("an ambitus")
127    /// };
128    /// assert_eq!(span.short_name(), "P8");
129    ///
130    /// let Some(Analysis::Key(key)) = line.analyze("key")? else {
131    ///     panic!("a key")
132    /// };
133    /// assert_eq!(key.tonic_pitch_name_with_case(), "C");
134    ///
135    /// // A stream with no pitches has no ambitus.
136    /// assert!(Stream::new().analyze("ambitus")?.is_none());
137    /// # Ok::<(), music21_rs::Error>(())
138    /// ```
139    ///
140    /// A stream with no pitches has no ambitus and answers nothing for
141    /// one, as music21 answers `None`; a key analysis of one refuses,
142    /// which is what music21 raises on.
143    ///
144    /// # Errors
145    ///
146    /// A method name no analysis answers to, as music21 raises on one, or
147    /// a key no weighting could settle on.
148    pub fn analyze(&self, method: &str) -> Result<Option<Analysis>> {
149        // music21 kept `range` as another name for `span` when it took
150        // getPitchRanges out.
151        let asked = if method == "range" { "span" } else { method };
152        let Some(found) = method_named(asked) else {
153            return Err(Error::Analysis(format!(
154                "no such analysis method: {method}"
155            )));
156        };
157        match found {
158            Method::Ambitus => {
159                let Some((lowest, highest)) = stream_pitch_span(self) else {
160                    // music21 answers None rather than refusing.
161                    return Ok(None);
162                };
163                Ok(Some(Analysis::Ambitus(Interval::between_pitches(
164                    &lowest, &highest,
165                )?)))
166            }
167            Method::Key(profile) => {
168                let estimates = estimate_key_of_stream(profile, self).ok_or_else(|| {
169                    Error::Analysis("a stream with no pitches has no key".to_string())
170                })?;
171                let best = estimates.first().ok_or_else(|| {
172                    Error::Analysis("no key was estimated for the stream".to_string())
173                })?;
174                Ok(Some(Analysis::Key(best.key().clone())))
175            }
176        }
177    }
178
179    /// Whether every pitch sounding is one of the twelve of equal
180    /// temperament: music21's `isTwelveTone`.
181    ///
182    /// A stream with no pitches is, as music21 answers for one.
183    ///
184    /// ```
185    /// use music21_rs::{Note, Stream};
186    ///
187    /// let mut line = Stream::new();
188    /// line.push(Note::from_name("G#4")?);
189    /// assert!(line.is_twelve_tone());
190    /// line.push(Note::from_name("G~4")?);
191    /// assert!(!line.is_twelve_tone());
192    /// # Ok::<(), music21_rs::Error>(())
193    /// ```
194    pub fn is_twelve_tone(&self) -> bool {
195        self.pitches()
196            .iter()
197            .all(crate::pitch::Pitch::is_twelve_tone)
198    }
199
200    /// Every note, rest and chord on the flattened timeline: music21's
201    /// `flatten().notesAndRests`.
202    ///
203    /// [`Self::notes`] leaves the rests out, as music21's `notes` does.
204    /// For what the stream holds itself rather than the whole nesting, ask
205    /// [`Self::elements_of_class`] for `GeneralNote`, which is music21's
206    /// own `notesAndRests`.
207    ///
208    /// ```
209    /// use music21_rs::{Duration, KeySignature, Note, Rest, Stream};
210    ///
211    /// let mut line = Stream::new();
212    /// line.insert(0.0, KeySignature::new(2));
213    /// line.push(Note::from_name("C4")?);
214    /// line.push(Rest::new(Duration::quarter()));
215    /// // The key signature is left out and the rest kept.
216    /// assert_eq!(line.notes_and_rests().len(), 2);
217    /// assert_eq!(line.notes().len(), 1);
218    /// # Ok::<(), music21_rs::Error>(())
219    /// ```
220    pub fn notes_and_rests(&self) -> Vec<(FloatType, StreamElement)> {
221        self.flatten()
222            .events()
223            .iter()
224            .filter(|event| is_general_note(event))
225            .map(|event| (event.offset(), event.element().clone()))
226            .collect()
227    }
228}
229
230/// Whether an element is one of music21's `GeneralNote`s.
231fn is_general_note(event: &StreamEvent) -> bool {
232    event.element().is_instance("GeneralNote")
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use crate::key::KeySignature;
239    use crate::note::Note;
240    use crate::rest::Rest;
241
242    fn triad() -> Stream {
243        let mut line = Stream::new();
244        for name in ["C4", "E4", "G4", "C5"] {
245            line.push(Note::from_name(name).expect("a note name"));
246        }
247        line
248    }
249
250    /// Each answer here is music21's own for the same stream.
251    #[test]
252    fn a_method_name_asks_for_an_analysis() {
253        let line = triad();
254        for name in ["ambitus", "span", "range", "Ambitus"] {
255            let Some(Analysis::Ambitus(span)) = line.analyze(name).expect("an ambitus") else {
256                panic!("an ambitus for {name}")
257            };
258            assert_eq!(span.short_name(), "P8");
259        }
260        for name in ["key", "keyscape", "aarden", "key.aarden"] {
261            let Some(Analysis::Key(key)) = line.analyze(name).expect("a key") else {
262                panic!("a key for {name}")
263            };
264            assert_eq!(key.tonic_pitch_name_with_case(), "C");
265        }
266        assert!(line.analyze("unknown-format").is_err());
267        // A stream with no pitches has no ambitus but refuses a key, as
268        // music21 answers None for the one and raises for the other.
269        let bare = Stream::new();
270        assert!(bare.analyze("ambitus").expect("no ambitus").is_none());
271        assert!(bare.analyze("key").is_err());
272    }
273
274    #[test]
275    fn a_loose_name_finds_what_music21_finds() {
276        // Every one of these is the profile music21's own search lands on.
277        for (name, profile) in [
278            ("kostka", "TemperleyKostkaPayne"),
279            ("payne", "TemperleyKostkaPayne"),
280            ("kessler", "KrumhanslSchmuckler"),
281            ("schmuckler", "KrumhanslSchmuckler"),
282            ("budge", "BellmanBudge"),
283            ("weight", "SimpleWeights"),
284            ("essen", "AardenEssen"),
285        ] {
286            let found = method_named(name).unwrap_or_else(|| panic!("{name} names an analysis"));
287            assert_eq!(found.class_name(), profile, "{name}");
288        }
289    }
290
291    #[test]
292    fn every_pitch_of_a_twelve_tone_stream_is_one_of_the_twelve() {
293        let mut line = Stream::new();
294        assert!(line.is_twelve_tone());
295        line.push(Note::from_name("G#4").expect("a note name"));
296        assert!(line.is_twelve_tone());
297        line.push(Note::from_name("G~4").expect("a note name"));
298        assert!(!line.is_twelve_tone());
299    }
300
301    #[test]
302    fn the_rests_come_with_the_notes_and_the_marks_do_not() {
303        let mut line = Stream::new();
304        line.insert(0.0, KeySignature::new(2));
305        line.push(Note::from_name("C4").expect("a note name"));
306        line.push(Rest::new(crate::duration::Duration::quarter()));
307        assert_eq!(line.notes_and_rests().len(), 2);
308        assert_eq!(line.notes().len(), 1);
309    }
310}