Skip to main content

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