music21/stream/consecutive.rs
1//! The notes that follow one another in a stream, and the intervals between
2//! them: music21's `Stream.findConsecutiveNotes` and `melodicIntervals`.
3
4use super::{Stream, StreamElement};
5use crate::defaults::FloatType;
6use crate::error::Result;
7use crate::interval::Interval;
8use crate::makenotation::op_frac;
9use crate::pitch::Pitch;
10
11/// Which notes [`Stream::find_consecutive_notes`] passes over, and where it
12/// marks a break: music21's keyword arguments to `findConsecutiveNotes`.
13/// Every option is off by default.
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15#[non_exhaustive]
16pub struct ConsecutiveOptions {
17 /// Whether a rest is passed over rather than breaking the line:
18 /// `skipRests`.
19 pub skip_rests: bool,
20 /// Whether a chord breaks the line rather than joining it:
21 /// `skipChords`.
22 pub skip_chords: bool,
23 /// Whether a note sounding the pitch of the note before, or a chord
24 /// sounding the pitches of the chord before, is passed over:
25 /// `skipUnisons`. Pitches are compared as sounded, so `F#` and `G-` are
26 /// one. What is passed over still sounds until it ends, so no break
27 /// follows it.
28 pub skip_unisons: bool,
29 /// Whether a note an octave or more from the note before, in the same
30 /// pitch class, is passed over too: `skipOctaves`, which implies
31 /// `skip_unisons`.
32 pub skip_octaves: bool,
33 /// Whether time with nothing in it is passed over rather than breaking
34 /// the line: `skipGaps`.
35 pub skip_gaps: bool,
36 /// Whether a note starting before the one before it ends is kept:
37 /// `getOverlaps`.
38 pub get_overlaps: bool,
39 /// Whether the list leaves out its breaks: `noNone`.
40 pub no_none: bool,
41}
42
43/// The interval from one note of a line to the next, as
44/// [`Stream::melodic_intervals`] finds it.
45#[derive(Clone, Debug, PartialEq)]
46pub struct MelodicInterval {
47 interval: Interval,
48 start: usize,
49 end: usize,
50 offset: FloatType,
51 quarter_length: FloatType,
52}
53
54impl MelodicInterval {
55 /// The interval, from the first note to the second; to a chord's first
56 /// note where either is a chord.
57 pub fn interval(&self) -> &Interval {
58 &self.interval
59 }
60
61 /// The position in [`Stream::leaves`] of the note the interval leaves.
62 pub fn start(&self) -> usize {
63 self.start
64 }
65
66 /// The position in [`Stream::leaves`] of the note the interval reaches.
67 pub fn end(&self) -> usize {
68 self.end
69 }
70
71 /// Where the interval begins, counted from the stream's start: where
72 /// the note it leaves ends.
73 pub fn offset(&self) -> FloatType {
74 self.offset
75 }
76
77 /// How long the interval lasts: from where the first note ends to where
78 /// the second starts, nothing for notes that touch.
79 pub fn quarter_length(&self) -> FloatType {
80 self.quarter_length
81 }
82}
83
84/// One stream in the walk, as music21's `recurse(streamsOnly=True,
85/// includeSelf=True)` hands it over.
86struct Container {
87 /// Where it stands in the stream holding it.
88 offset: FloatType,
89 /// How far its own contents reach.
90 highest_time: FloatType,
91 /// The elements it holds itself that are notes, rests and the like, each
92 /// with its offset in it and its position among the leaves.
93 notes: Vec<(FloatType, usize)>,
94}
95
96fn containers(stream: &Stream, offset: FloatType, position: &mut usize, out: &mut Vec<Container>) {
97 let index = out.len();
98 out.push(Container {
99 offset,
100 highest_time: stream.end_offset(),
101 notes: Vec::new(),
102 });
103 for event in &stream.events {
104 match &event.element {
105 StreamElement::Stream(inner) => containers(inner, event.offset, position, out),
106 element => {
107 if is_general_note(element) {
108 out[index].notes.push((event.offset, *position));
109 }
110 *position += 1;
111 }
112 }
113 }
114}
115
116/// music21's `GeneralNote`: anything written that sounds or rests.
117fn is_general_note(element: &StreamElement) -> bool {
118 matches!(
119 element,
120 StreamElement::Note(_)
121 | StreamElement::Chord(_)
122 | StreamElement::Rest(_)
123 | StreamElement::Unpitched(_)
124 | StreamElement::PercussionChord(_)
125 | StreamElement::ChordSymbol(_)
126 )
127}
128
129/// The pitches a chord sounds, for a chord or a chord symbol.
130fn chord_pitches(element: &StreamElement) -> Result<Option<Vec<Pitch>>> {
131 Ok(match element {
132 StreamElement::Chord(chord) => Some(chord.pitches()),
133 StreamElement::ChordSymbol(symbol) => Some(symbol.pitches()?),
134 _ => None,
135 })
136}
137
138impl Stream {
139 /// The notes and chords that follow one another in this stream, as
140 /// positions in [`Stream::leaves`], with `None` wherever the line breaks:
141 /// music21's `findConsecutiveNotes`.
142 ///
143 /// Each stream is read on its own, its contents in order, and the line
144 /// breaks at a rest, at time with nothing in it, at a chord when chords
145 /// are skipped, and where one stream starts before the one read before
146 /// it ended -- the second voice of a measure, the second part of a
147 /// score. A note starting before the one before it ends is left out.
148 /// `options` changes each of those. A chord of one note is passed over,
149 /// as are unpitched strokes; a chord symbol counts as a chord. The list
150 /// never ends on a break.
151 ///
152 /// # Errors
153 ///
154 /// A chord symbol whose notes cannot be worked out.
155 pub fn find_consecutive_notes(
156 &self,
157 options: &ConsecutiveOptions,
158 ) -> Result<Vec<Option<usize>>> {
159 let leaves = self.leaves();
160 let mut walk = Vec::new();
161 containers(self, 0.0, &mut 0, &mut walk);
162
163 let skip_unisons = options.skip_unisons || options.skip_octaves;
164 let mut found: Vec<Option<usize>> = Vec::new();
165 let mut last_container_end: FloatType = 0.0;
166 let mut last_was_none = false;
167 let mut last_pitches: Vec<Pitch> = Vec::new();
168
169 for container in &walk {
170 let holds_notes = !container.notes.is_empty();
171 if container.offset < last_container_end && holds_notes && !options.no_none {
172 found.push(None);
173 last_was_none = true;
174 last_pitches.clear();
175 }
176 let mut last_end: FloatType = 0.0;
177 if holds_notes {
178 last_container_end = container.highest_time;
179 }
180 for &(offset, position) in &container.notes {
181 let element = leaves[position].1;
182 if !last_was_none && !options.skip_gaps && offset > last_end && !options.no_none {
183 found.push(None);
184 last_was_none = true;
185 }
186 let length = element.quarter_length();
187 if let StreamElement::Note(note) = element {
188 let pitch = note.pitch();
189 // A unison is judged against a single note only. One
190 // passed over still sounds until it ends.
191 if skip_unisons
192 && last_pitches.len() == 1
193 && pitch.pitch_class() == last_pitches[0].pitch_class()
194 && (options.skip_octaves || pitch.ps() == last_pitches[0].ps())
195 {
196 last_end = last_end.max(op_frac(offset + length));
197 continue;
198 }
199 if !options.get_overlaps && offset < last_end {
200 continue;
201 }
202 found.push(Some(position));
203 if offset < last_end {
204 continue;
205 }
206 last_end = op_frac(offset + length);
207 last_was_none = false;
208 last_pitches = vec![pitch.clone()];
209 } else if let Some(pitches) = chord_pitches(element)? {
210 if pitches.len() <= 1 {
211 continue;
212 }
213 if options.skip_chords {
214 if !last_was_none && !options.no_none {
215 found.push(None);
216 last_was_none = true;
217 last_pitches.clear();
218 }
219 } else if skip_unisons && same_sounds(&pitches, &last_pitches) {
220 // A repeated chord is passed over, but still sounds
221 // until it ends.
222 last_end = last_end.max(op_frac(offset + length));
223 } else if options.get_overlaps || offset >= last_end {
224 found.push(Some(position));
225 if offset < last_end {
226 continue;
227 }
228 last_end = op_frac(offset + length);
229 last_pitches = pitches;
230 last_was_none = false;
231 }
232 } else if let StreamElement::Rest(_) = element {
233 if options.skip_rests {
234 last_end = op_frac(offset + length);
235 } else if !last_was_none && !options.no_none {
236 found.push(None);
237 last_was_none = true;
238 last_pitches.clear();
239 }
240 }
241 }
242 }
243 if last_was_none {
244 found.pop();
245 }
246 Ok(found)
247 }
248
249 /// The interval between each two notes [`Stream::find_consecutive_notes`]
250 /// finds next to each other, with no break between them: music21's
251 /// `melodicIntervals`. An interval to or from a chord is taken from
252 /// the chord's first note.
253 ///
254 /// ```
255 /// use music21::stream::ConsecutiveOptions;
256 /// use music21::tinynotation::from_tiny_notation;
257 ///
258 /// let line = from_tiny_notation("3/4 c4 d' r b b'")?;
259 /// let names: Vec<String> = line
260 /// .melodic_intervals(&ConsecutiveOptions::default())?
261 /// .iter()
262 /// .map(|step| step.interval().short_name())
263 /// .collect();
264 /// assert_eq!(names, ["M9", "P8"]);
265 /// # Ok::<(), music21::Error>(())
266 /// ```
267 ///
268 /// # Errors
269 ///
270 /// As [`Stream::find_consecutive_notes`], or two notes no interval can
271 /// be spelled between.
272 pub fn melodic_intervals(&self, options: &ConsecutiveOptions) -> Result<Vec<MelodicInterval>> {
273 let found = self.find_consecutive_notes(options)?;
274 let leaves = self.leaves();
275 let mut intervals = Vec::new();
276 for pair in found.windows(2) {
277 let (Some(start), Some(end)) = (pair[0], pair[1]) else {
278 continue;
279 };
280 let (Some(from), Some(to)) =
281 (first_pitch(leaves[start].1)?, first_pitch(leaves[end].1)?)
282 else {
283 continue;
284 };
285 let offset = op_frac(leaves[start].0 + leaves[start].1.quarter_length());
286 intervals.push(MelodicInterval {
287 interval: Interval::between_pitches(&from, &to)?,
288 start,
289 end,
290 offset,
291 quarter_length: op_frac(leaves[end].0 - offset),
292 });
293 }
294 Ok(intervals)
295 }
296
297 /// For each note this stream holds itself, by its index here, the
298 /// interval from it to the first note of `other` sounding where it
299 /// starts -- starting there, or sounding on to it or ending there --
300 /// that an interval can be taken to: what music21's
301 /// `attachIntervalsBetweenStreams` attaches to each note as its
302 /// `harmonicInterval`. `None` where no note of `other` sounds.
303 ///
304 /// ```
305 /// use music21::tinynotation::from_tiny_notation;
306 ///
307 /// let upper = from_tiny_notation("4/4 e'2 d'2")?.flatten();
308 /// let lower = from_tiny_notation("4/4 c1")?.flatten();
309 /// let names: Vec<_> = upper
310 /// .harmonic_intervals(&lower)
311 /// .into_iter()
312 /// .map(|(_, interval)| interval.map(|interval| interval.name()))
313 /// .collect();
314 /// let expected = ["Major Tenth", "Major Ninth"].map(|name| Some(name.to_string()));
315 /// assert_eq!(names, expected);
316 /// # Ok::<(), music21::Error>(())
317 /// ```
318 pub fn harmonic_intervals(&self, other: &Stream) -> Vec<(usize, Option<Interval>)> {
319 self.events
320 .iter()
321 .enumerate()
322 .filter_map(|(index, event)| match event.element() {
323 StreamElement::Note(note) => Some((index, event.offset(), note)),
324 _ => None,
325 })
326 .map(|(index, offset, note)| {
327 let sounding = super::span::OffsetSpan {
328 must_begin_in_span: false,
329 ..super::span::OffsetSpan::at(offset)
330 };
331 let interval = other
332 .events
333 .iter()
334 .filter(|event| {
335 sounding.takes_at(event.offset(), event.element().quarter_length())
336 })
337 .filter_map(|event| match event.element() {
338 StreamElement::Note(other) => Interval::between_notes(note, other).ok(),
339 _ => None,
340 })
341 .next();
342 (index, interval)
343 })
344 .collect()
345 }
346}
347
348/// Whether two chords sound the same pitches, in any order.
349fn same_sounds(pitches: &[Pitch], other: &[Pitch]) -> bool {
350 let sorted = |pitches: &[Pitch]| {
351 let mut sounds: Vec<FloatType> = pitches.iter().map(Pitch::ps).collect();
352 sounds.sort_by(FloatType::total_cmp);
353 sounds
354 };
355 pitches.len() == other.len() && sorted(pitches) == sorted(other)
356}
357
358/// A note's pitch, or a chord's first note's.
359fn first_pitch(element: &StreamElement) -> Result<Option<Pitch>> {
360 Ok(match element {
361 StreamElement::Note(note) => Some(note.pitch().clone()),
362 element => chord_pitches(element)?.and_then(|pitches| pitches.into_iter().next()),
363 })
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use crate::tinynotation::from_tiny_notation;
370
371 #[test]
372 fn a_skipped_unison_does_not_break_the_line() {
373 let line = from_tiny_notation("4/4 c4 c4 d4 e4").unwrap();
374 let options = ConsecutiveOptions {
375 skip_unisons: true,
376 ..ConsecutiveOptions::default()
377 };
378 let found = line.find_consecutive_notes(&options).unwrap();
379 assert_eq!(found.len(), 3);
380 assert!(found.iter().all(Option::is_some));
381 }
382}