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}