Skip to main content

music21_rs/stream/
chordify.rs

1//! A score's parts as one line of chords: music21's `Stream.chordify` and
2//! the `Verticality.makeElement` it builds each chord with.
3
4use super::{Stream, StreamElement, StreamEvent, StreamKind};
5use crate::articulations::Articulation;
6use crate::chord::Chord;
7use crate::defaults::FloatType;
8use crate::duration::Duration;
9use crate::error::Result;
10use crate::expressions::Expression;
11use crate::makenotation::op_frac;
12use crate::notation::{StemDirection, Tie, TieType};
13use crate::note::Note;
14use crate::percussion::PercussionNote;
15use crate::rest::Rest;
16
17/// How [`Stream::chordify_with`] makes its chords: music21's keyword
18/// arguments to `chordify`.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub struct ChordifyOptions {
21    /// Whether a note sounding on from one chord into the next is tied
22    /// across: music21's `addTies`.
23    pub add_ties: bool,
24    /// Whether a pitch two parts sound at once is one note of the chord
25    /// rather than two: music21's `removeRedundantPitches`.
26    pub remove_redundant_pitches: bool,
27    /// Whether a score written at the pitch its instruments read is turned
28    /// to the pitch it sounds first: music21's `toSoundingPitch`.
29    pub to_sounding_pitch: bool,
30}
31
32impl Default for ChordifyOptions {
33    fn default() -> Self {
34        Self {
35            add_ties: true,
36            remove_redundant_pitches: true,
37            to_sounding_pitch: true,
38        }
39    }
40}
41
42/// One element sounding over a stretch of time: music21's timespan.
43pub(crate) struct Span<'a> {
44    pub(crate) start: FloatType,
45    pub(crate) end: FloatType,
46    pub(crate) element: &'a StreamElement,
47    /// Which of the streams read the element came from.
48    pub(crate) part: usize,
49}
50
51impl Stream {
52    /// The stream as one line of chords, as music21's `chordify` makes it:
53    /// [`Stream::chordify_with`] with music21's defaults.
54    ///
55    /// ```
56    /// use music21_rs::{Duration, Note, Stream, StreamKind};
57    ///
58    /// let mut upper = Stream::with_kind(StreamKind::Part);
59    /// upper.push(Note::from_name("E4")?.with_duration(Duration::half()));
60    /// let mut lower = Stream::with_kind(StreamKind::Part);
61    /// lower.push(Note::from_name("C4")?.with_duration(Duration::quarter()));
62    /// lower.push(Note::from_name("G3")?.with_duration(Duration::quarter()));
63    /// let mut score = Stream::with_kind(StreamKind::Score);
64    /// score.insert(0.0, upper);
65    /// score.insert(0.0, lower);
66    ///
67    /// let chords = score.chordify()?;
68    /// let names: Vec<String> = chords
69    ///     .pitches()
70    ///     .iter()
71    ///     .map(|pitch| pitch.name_with_octave())
72    ///     .collect();
73    /// assert_eq!(names, ["C4", "E4", "G3", "E4"]);
74    /// # Ok::<(), music21_rs::Error>(())
75    /// ```
76    ///
77    /// # Errors
78    ///
79    /// A score that cannot be turned to sounding pitch, or a length no
80    /// duration has.
81    pub fn chordify(&self) -> Result<Stream> {
82        self.chordify_with(&ChordifyOptions::default())
83    }
84
85    /// The stream as one line of chords: at every point where a note,
86    /// chord or rest in any part starts or stops, a chord of every pitch
87    /// sounding there, or a rest where none is.
88    ///
89    /// The result is laid out as the first part is -- its measures, clefs,
90    /// keys, meters and the rest of what it holds, but none of its notes,
91    /// rests or voices -- and each measure is made from the measure at the
92    /// same place in every part. A note sounding on past the end of a chord
93    /// is tied into the next, a pitch sounding in two parts at once is one
94    /// note, and a chord carries the articulations and expressions its
95    /// notes do, one of each kind. Rests standing together in a measure are
96    /// one rest. A chord symbol's notes sound where it stands, as music21's
97    /// do. Spanners and the lyrics of the notes are not carried across, and
98    /// every accidental is left for the notation to decide again.
99    ///
100    /// # Errors
101    ///
102    /// As [`Stream::chordify`].
103    pub fn chordify_with(&self, options: &ChordifyOptions) -> Result<Stream> {
104        Ok(self.chordify_tracking(options)?.0)
105    }
106
107    /// The stream chordified with every pitch kept, and how many parts
108    /// sound a note in each chord made, alone or in a chord, in order,
109    /// parts sharing an id counted once: the groups music21's
110    /// `chordify(addPartIdAsGroup=True)` writes on the chord's pitches.
111    pub(crate) fn chordify_parts_sounding(&self) -> Result<(Stream, Vec<usize>)> {
112        let options = ChordifyOptions {
113            remove_redundant_pitches: false,
114            ..ChordifyOptions::default()
115        };
116        let ids: Vec<Option<String>> = self
117            .events
118            .iter()
119            .filter_map(|event| match &event.element {
120                StreamElement::Stream(inner) if inner.kind() != StreamKind::Voice => {
121                    Some(inner.id().map(str::to_string))
122                }
123                _ => None,
124            })
125            .collect();
126        let (chordified, heard) = self.chordify_tracking(&options)?;
127        let sounding = heard
128            .into_iter()
129            .map(|parts| {
130                let mut named: Vec<String> = Vec::new();
131                for part in parts {
132                    let name = ids
133                        .get(part)
134                        .cloned()
135                        .flatten()
136                        .unwrap_or_else(|| format!("#{part}"));
137                    if !named.contains(&name) {
138                        named.push(name);
139                    }
140                }
141                named.len()
142            })
143            .collect();
144        Ok((chordified, sounding))
145    }
146
147    /// The chords and, beside them, the streams sounding in each chord made.
148    fn chordify_tracking(&self, options: &ChordifyOptions) -> Result<(Stream, Vec<Vec<usize>>)> {
149        let mut heard: Vec<Vec<usize>> = Vec::new();
150        let first_stream = |stream: &Stream| -> Option<Stream> {
151            stream.events.iter().find_map(|event| match &event.element {
152                StreamElement::Stream(inner) => Some((**inner).clone()),
153                _ => None,
154            })
155        };
156        let parted = self.has_part_like_streams();
157        let work = if options.to_sounding_pitch
158            && ((parted
159                && first_stream(self)
160                    .is_some_and(|first| first.at_sounding_pitch() == Some(false)))
161                || (!parted && self.at_sounding_pitch() == Some(false)))
162        {
163            self.to_sounding_pitch()?
164        } else {
165            self.clone()
166        };
167        let source = if parted {
168            first_stream(&work).unwrap_or_else(|| work.clone())
169        } else {
170            work.clone()
171        };
172        let mut template = template_of(&source);
173
174        if template.measures().is_empty() {
175            let spans = spans_of(&[&work]);
176            fill(&mut template, &spans, options, &mut heard)?;
177        } else {
178            // Each part's measures, by their place among its measures.
179            let parts: Vec<&Stream> = if parted {
180                work.events
181                    .iter()
182                    .filter_map(|event| match &event.element {
183                        StreamElement::Stream(inner) if inner.kind() != StreamKind::Voice => {
184                            Some(&**inner)
185                        }
186                        _ => None,
187                    })
188                    .collect()
189            } else {
190                vec![&work]
191            };
192            let measures_of: Vec<Vec<&Stream>> = parts.iter().map(|part| part.measures()).collect();
193            let mut index = 0;
194            for event in &mut template.events {
195                let StreamElement::Stream(measure) = &mut event.element else {
196                    continue;
197                };
198                if measure.kind() != StreamKind::Measure {
199                    continue;
200                }
201                let sources: Vec<&Stream> = measures_of
202                    .iter()
203                    .filter_map(|measures| measures.get(index).copied())
204                    .collect();
205                index += 1;
206                let spans = spans_of(&sources);
207                fill(measure, &spans, options, &mut heard)?;
208            }
209        }
210
211        // Every accidental is decided afresh by whatever writes the chords.
212        template.for_each_mut(&mut |_, element| {
213            let notes = match element {
214                StreamElement::Chord(chord) => chord.notes_mut(),
215                StreamElement::Note(note) => std::slice::from_mut(note),
216                _ => return,
217            };
218            for note in notes {
219                let mut pitch = note.pitch().clone();
220                if let Some(accidental) = pitch.written_accidental_mut()
221                    && accidental.display_type() != "even-tied"
222                {
223                    accidental.set_display_status(None);
224                    note.set_pitch(pitch);
225                }
226            }
227        });
228        if parted && let Some(metadata) = work.metadata() {
229            template.set_metadata(Some(metadata.clone()));
230        }
231        Ok((template, heard))
232    }
233}
234
235/// Whether an element brings a pitched note to a chord made: a note, or a
236/// chord with a note among its members.
237fn sounds_a_note(element: &StreamElement) -> bool {
238    match element {
239        StreamElement::Note(_) => true,
240        StreamElement::Chord(chord) => !chord.notes().is_empty(),
241        StreamElement::ChordSymbol(symbol) => {
242            symbol.pitches().is_ok_and(|pitches| !pitches.is_empty())
243        }
244        StreamElement::PercussionChord(chord) => chord
245            .members()
246            .iter()
247            .any(|member| matches!(member, PercussionNote::Note(_))),
248        _ => false,
249    }
250}
251
252/// Whether an element is one of music21's `GeneralNote`s, which chordify
253/// takes out of the template and reads the time of.
254fn is_general_note(element: &StreamElement) -> bool {
255    matches!(
256        element,
257        StreamElement::Note(_)
258            | StreamElement::Chord(_)
259            | StreamElement::Rest(_)
260            | StreamElement::Unpitched(_)
261            | StreamElement::PercussionChord(_)
262            | StreamElement::ChordSymbol(_)
263    )
264}
265
266/// music21's `template(fillWithRests=False, removeClasses=('GeneralNote',),
267/// retainVoices=False)`: the stream and every stream inside it, voices left
268/// out, with everything but notes, chords and rests kept where it stood.
269pub(crate) fn template_of(stream: &Stream) -> Stream {
270    template_removing(stream, false)
271}
272
273/// The same, removing the dynamics and expressions as well where `marks`
274/// says to: that is music21's `template` with no `removeClasses` of its
275/// own, which removes `GeneralNote`, `Dynamic` and `Expression`.
276/// `chordify` names `GeneralNote` alone and so keeps them;
277/// `tree.toStream.chordified`, which the chord reducer goes through, does
278/// not, and so loses them.
279pub(crate) fn template_removing(stream: &Stream, marks: bool) -> Stream {
280    let events = stream
281        .events
282        .iter()
283        .filter_map(|event| match &event.element {
284            StreamElement::Stream(inner) if inner.kind() == StreamKind::Voice => None,
285            StreamElement::Stream(inner) => Some(StreamEvent::new(
286                event.offset,
287                template_removing(inner, marks),
288            )),
289            element if is_general_note(element) => None,
290            element
291                if marks
292                    && (element.is_instance("Dynamic") || element.is_instance("Expression")) =>
293            {
294                None
295            }
296            _ => Some(event.clone()),
297        })
298        .collect();
299    // music21's `cloneEmpty` keeps a measure's number and padding and a
300    // part's name, but not whether either is shown, nor how measures are
301    // numbered.
302    let mut template = stream.with_events(events);
303    template.set_number_hidden(false);
304    template.set_measure_numbering(None);
305    template.set_name_hidden(false);
306    template.set_abbreviation_hidden(false);
307    template
308}
309
310/// music21's `asTimespans(classList=(GeneralNote,))` over the streams, one
311/// after another, each read from its own start.
312fn spans_of<'a>(streams: &[&'a Stream]) -> Vec<Span<'a>> {
313    let mut spans = Vec::new();
314    for (part, stream) in streams.iter().enumerate() {
315        for (offset, element) in stream.leaves() {
316            if is_general_note(element) {
317                let start = op_frac(offset);
318                spans.push(Span {
319                    start,
320                    end: op_frac(start + element.quarter_length()),
321                    element,
322                    part,
323                });
324            }
325        }
326    }
327    spans
328}
329
330/// `chordifyOneMeasure`: a chord or rest at every time point of the spans,
331/// put in the stream, and the rests standing together there joined.
332fn fill(
333    stream: &mut Stream,
334    spans: &[Span<'_>],
335    options: &ChordifyOptions,
336    heard: &mut Vec<Vec<usize>>,
337) -> Result<()> {
338    let mut points: Vec<FloatType> = spans
339        .iter()
340        .flat_map(|span| [span.start, span.end])
341        .collect();
342    points.sort_by(FloatType::total_cmp);
343    points.dedup();
344    if !points.contains(&0.0) {
345        points.insert(0, 0.0);
346    }
347    let mut made = Vec::new();
348    for pair in points.windows(2) {
349        let (offset, end) = (pair[0], pair[1]);
350        if (end - offset).abs() <= 1e-7 {
351            continue;
352        }
353        let element = make_element(spans, offset, op_frac(end - offset), options)?;
354        if matches!(element, StreamElement::Chord(_)) {
355            let mut parts = Vec::new();
356            for span in sounding_at(spans, offset) {
357                if sounds_a_note(span.element) && !parts.contains(&span.part) {
358                    parts.push(span.part);
359                }
360            }
361            heard.push(parts);
362        }
363        made.push(StreamEvent::new(op_frac(offset), element));
364    }
365    stream.insert_sorted(made);
366    consolidate_rests(stream)
367}
368
369/// The spans sounding at `offset`, as a verticality lists them: those
370/// starting there by where they end, then those started earlier by where
371/// they start and end.
372fn sounding_at<'a, 'b>(spans: &'b [Span<'a>], offset: FloatType) -> Vec<&'b Span<'a>> {
373    let mut starting: Vec<&Span<'a>> = spans.iter().filter(|span| span.start == offset).collect();
374    starting.sort_by(|left, right| left.end.total_cmp(&right.end));
375    let mut overlapping: Vec<&Span<'a>> = spans
376        .iter()
377        .filter(|span| span.start < offset && offset < span.end)
378        .collect();
379    overlapping.sort_by(|left, right| {
380        left.start
381            .total_cmp(&right.start)
382            .then(left.end.total_cmp(&right.end))
383    });
384    starting.extend(overlapping);
385    starting
386}
387
388/// The notes an element sounds, each with the articulations and expressions
389/// of the chord as a whole added to its first: what `makeElement` reads.
390fn notes_of(element: &StreamElement) -> Result<Vec<Note>> {
391    Ok(match element {
392        StreamElement::Note(note) => vec![note.clone()],
393        StreamElement::Chord(chord) => with_chord_marks(
394            chord.notes().to_vec(),
395            chord.articulations(),
396            chord.expressions(),
397        ),
398        StreamElement::ChordSymbol(symbol) => {
399            let duration = symbol.duration().clone();
400            symbol
401                .pitches()?
402                .into_iter()
403                .map(|pitch| Note::from_pitch(pitch).with_duration(duration.clone()))
404                .collect()
405        }
406        StreamElement::PercussionChord(chord) => {
407            let written = chord.written();
408            let notes = chord
409                .members()
410                .into_iter()
411                .filter_map(|member| match member {
412                    PercussionNote::Note(note) => Some(note),
413                    PercussionNote::Unpitched(_) => None,
414                })
415                .collect();
416            with_chord_marks(notes, written.articulations(), written.expressions())
417        }
418        _ => Vec::new(),
419    })
420}
421
422fn with_chord_marks(
423    mut notes: Vec<Note>,
424    articulations: &[Articulation],
425    expressions: &[Expression],
426) -> Vec<Note> {
427    if let Some(first) = notes.first_mut() {
428        first
429            .articulations_mut()
430            .extend(articulations.iter().cloned());
431        first.expressions_mut().extend(expressions.iter().cloned());
432    }
433    notes
434}
435
436/// music21's `Verticality.makeElement`: a chord of every pitch sounding at
437/// `offset`, lasting `length`, or a rest where nothing sounds.
438pub(crate) fn make_element(
439    spans: &[Span<'_>],
440    offset: FloatType,
441    length: FloatType,
442    options: &ChordifyOptions,
443) -> Result<StreamElement> {
444    let duration = Duration::new(length)?;
445    let sounding = sounding_at(spans, offset);
446    let mut notes: Vec<(String, Note)> = Vec::new();
447    let mut bust = 0;
448    let mut any_pitch = false;
449    for span in &sounding {
450        for note in notes_of(span.element)? {
451            any_pitch = true;
452            let key = note.pitch().name_with_octave();
453            let made = || new_note(span, &note, &duration, offset, length, options);
454            match notes.iter().position(|(known, _)| *known == key) {
455                None => notes.push((key, made())),
456                Some(_) if !options.remove_redundant_pitches => {
457                    notes.push((format!("{key}{bust}"), made()));
458                    bust += 1;
459                }
460                Some(_) if !options.add_ties => {}
461                Some(place) => {
462                    let old = notes[place].1.tie().map(Tie::tie_type);
463                    if old == Some(TieType::Continue) {
464                        continue;
465                    }
466                    let candidate = made();
467                    let Some(new) = candidate.tie().map(Tie::tie_type) else {
468                        continue;
469                    };
470                    match old {
471                        None => notes[place].1 = candidate,
472                        Some(old) if is_start_and_stop(old, new) => {
473                            if let Some(mut tie) = notes[place].1.tie().cloned() {
474                                tie.set_tie_type(TieType::Continue);
475                                notes[place].1.set_tie(Some(tie));
476                            }
477                        }
478                        Some(_) if new == TieType::Continue => notes[place].1 = candidate,
479                        Some(_) => {}
480                    }
481                }
482            }
483        }
484    }
485    if !any_pitch {
486        return Ok(StreamElement::Rest(Rest::new(duration)));
487    }
488
489    let mut ordered: Vec<Note> = notes.into_iter().map(|(_, note)| note).collect();
490    // music21 adds the notes by pitch space, and its `Chord.add` sorts the
491    // chord by staff step and then pitch space after each: `D#4` before
492    // `E-4`. Both sorts are stable, as Python's are.
493    ordered.sort_by(|left, right| left.pitch().ps().total_cmp(&right.pitch().ps()));
494    ordered.sort_by(|left, right| {
495        left.pitch()
496            .diatonic_note_number()
497            .cmp(&right.pitch().diatonic_note_number())
498            .then(left.pitch().ps().total_cmp(&right.pitch().ps()))
499    });
500    let mut articulations: Vec<Articulation> = Vec::new();
501    let mut expressions: Vec<Expression> = Vec::new();
502    for note in &ordered {
503        let tie = note.tie().map(Tie::tie_type);
504        let kept = |attach: &str| match attach {
505            "first" => tie.is_none_or(|tie| tie == TieType::Start),
506            "last" => tie.is_none_or(|tie| tie == TieType::Stop),
507            _ => true,
508        };
509        for articulation in note.articulations() {
510            if kept(articulation.tie_attach())
511                && !articulations
512                    .iter()
513                    .any(|seen| seen.kind() == articulation.kind())
514            {
515                articulations.push(articulation.clone());
516            }
517        }
518        for expression in note.expressions() {
519            if kept(expression.tie_attach())
520                && !expressions
521                    .iter()
522                    .any(|seen| same_kind_of_expression(seen, expression))
523            {
524                expressions.push(expression.clone());
525            }
526        }
527    }
528    let mut chord = Chord::new(ordered)?.with_duration(duration);
529    chord.articulations_mut().extend(articulations);
530    chord.expressions_mut().extend(expressions);
531    Ok(StreamElement::Chord(chord))
532}
533
534fn is_start_and_stop(left: TieType, right: TieType) -> bool {
535    matches!(
536        (left, right),
537        (TieType::Start, TieType::Stop) | (TieType::Stop, TieType::Start)
538    )
539}
540
541/// music21 keeps one expression of each class.
542fn same_kind_of_expression(left: &Expression, right: &Expression) -> bool {
543    match (left, right) {
544        (Expression::Ornament(left), Expression::Ornament(right)) => left.kind() == right.kind(),
545        (Expression::Fermata(_), Expression::Fermata(_))
546        | (Expression::Arpeggio(_), Expression::Arpeggio(_)) => true,
547        _ => false,
548    }
549}
550
551/// `newNote`: a copy of the note lasting as long as the chord, tied as it
552/// sounds on from before or past it, its stem left to the chord and its
553/// lyrics left behind.
554fn new_note(
555    span: &Span<'_>,
556    note: &Note,
557    duration: &Duration,
558    offset: FloatType,
559    length: FloatType,
560    options: &ChordifyOptions,
561) -> Note {
562    let mut made = note.clone().with_duration(duration.clone());
563    made.lyrics_mut().clear();
564    if made.stem_direction() != StemDirection::NoStem {
565        made.set_stem_direction(StemDirection::Unspecified);
566    }
567    if !options.add_ties {
568        return made;
569    }
570    let before = op_frac(offset - span.start);
571    let after = op_frac(span.end - (offset + length));
572    let added = if before == 0.0 && after <= 0.0 {
573        None
574    } else if before > 0.0 {
575        Some(if after > 0.0 {
576            TieType::Continue
577        } else {
578            TieType::Stop
579        })
580    } else {
581        Some(TieType::Start)
582    };
583    match (made.tie().cloned(), added) {
584        (Some(mut tie), Some(added)) if is_start_and_stop(tie.tie_type(), added) => {
585            tie.set_tie_type(TieType::Continue);
586            made.set_tie(Some(tie));
587        }
588        (Some(mut tie), _) if tie.tie_type() == TieType::Continue => {
589            tie.set_placement(None);
590            made.set_tie(Some(tie));
591        }
592        (Some(mut tie), None) => {
593            tie.set_placement(None);
594            made.set_tie(Some(tie));
595        }
596        (_, Some(added)) => made.set_tie(Some(Tie::new(added))),
597        (None, None) => {}
598    }
599    made
600}
601
602/// `consolidateRests`: every run of rests standing one after another among
603/// the notes, chords and rests of the stream is one rest as long as all of
604/// them, where the first stood.
605fn consolidate_rests(stream: &mut Stream) -> Result<()> {
606    let sounding: Vec<usize> = (0..stream.events.len())
607        .filter(|place| {
608            matches!(
609                stream.events[*place].element,
610                StreamElement::Note(_) | StreamElement::Chord(_) | StreamElement::Rest(_)
611            )
612        })
613        .collect();
614    let mut runs: Vec<Vec<usize>> = Vec::new();
615    let mut run: Vec<usize> = Vec::new();
616    for place in sounding {
617        if matches!(stream.events[place].element, StreamElement::Rest(_)) {
618            run.push(place);
619        } else if !run.is_empty() {
620            runs.push(std::mem::take(&mut run));
621        }
622    }
623    runs.push(run);
624    let runs: Vec<Vec<usize>> = runs.into_iter().filter(|run| run.len() >= 2).collect();
625    if runs.is_empty() {
626        return Ok(());
627    }
628    let mut joined = Vec::new();
629    for run in &runs {
630        let total: FloatType = run
631            .iter()
632            .map(|place| stream.events[*place].element.quarter_length())
633            .sum();
634        let at = stream.events[run[0]].offset;
635        joined.push(StreamEvent::new(
636            at,
637            StreamElement::Rest(Rest::new(Duration::new(total)?)),
638        ));
639    }
640    let dropped: Vec<usize> = runs.into_iter().flatten().collect();
641    let mut place = 0;
642    stream.events.retain(|_| {
643        let keep = !dropped.contains(&place);
644        place += 1;
645        keep
646    });
647    stream.insert_sorted(joined);
648    Ok(())
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654    use crate::pitch::Pitch;
655
656    fn part(notes: &[(&str, FloatType)]) -> Stream {
657        let mut part = Stream::with_kind(StreamKind::Part);
658        for (name, length) in notes {
659            let element: StreamElement = if *name == "r" {
660                Rest::new(Duration::new(*length).unwrap()).into()
661            } else {
662                Note::from_name(name)
663                    .unwrap()
664                    .with_duration(Duration::new(*length).unwrap())
665                    .into()
666            };
667            part.push(element);
668        }
669        part
670    }
671
672    fn score(parts: Vec<Stream>) -> Stream {
673        let mut score = Stream::with_kind(StreamKind::Score);
674        for part in parts {
675            score.insert(0.0, part);
676        }
677        score
678    }
679
680    #[test]
681    fn a_held_note_is_tied_across_the_chords_it_sounds_through() {
682        let chords = score(vec![
683            part(&[("C5", 2.0)]),
684            part(&[("E4", 1.0), ("G4", 1.0)]),
685        ])
686        .chordify()
687        .unwrap();
688        let ties: Vec<Vec<Option<TieType>>> = chords
689            .events()
690            .iter()
691            .filter_map(|event| match event.element() {
692                StreamElement::Chord(chord) => Some(
693                    chord
694                        .notes()
695                        .iter()
696                        .map(|note| note.tie().map(Tie::tie_type))
697                        .collect(),
698                ),
699                _ => None,
700            })
701            .collect();
702        assert_eq!(
703            ties,
704            [
705                vec![None, Some(TieType::Start)],
706                vec![None, Some(TieType::Stop)]
707            ]
708        );
709    }
710
711    #[test]
712    fn a_pitch_two_parts_sound_is_one_note_and_rests_together_are_one_rest() {
713        let chords = score(vec![
714            part(&[("C4", 1.0), ("r", 1.0), ("r", 1.0)]),
715            part(&[("C4", 1.0), ("r", 2.0)]),
716        ])
717        .chordify()
718        .unwrap();
719        let events = chords.events();
720        assert_eq!(events.len(), 2);
721        let StreamElement::Chord(chord) = events[0].element() else {
722            panic!("a chord first");
723        };
724        assert_eq!(chord.pitches(), [Pitch::from_name("C4").unwrap()]);
725        assert!(matches!(events[1].element(), StreamElement::Rest(_)));
726        assert_eq!(events[1].element().quarter_length(), 2.0);
727    }
728}