Skip to main content

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