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}