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