Skip to main content

music21/analysis/
reduce_chords.rs

1//! A score reduced to the chords it is made of: music21's
2//! `analysis.reduceChords`.
3//!
4//! [`reduce_chords`] is music21's `ChordReducer.run`. It reads the score's
5//! notes as spans of time -- a part, a stretch, and the notes sounding in
6//! it -- drops the spans of no length and, if asked, the notes above the
7//! bass of a dissonant moment. Each measure of the first part is then cut
8//! into slices wherever a note starts or stops, and the slices sounding
9//! the same pitch classes are weighed together: how long each lasts, times
10//! how strong the beat it starts on is (the last sounding slice counting
11//! its length alone), times a tenth if it is dissonant. The few heaviest
12//! are kept, each lasting until the next kept slice of other classes. A
13//! chord that is the same as the first chord of the next measure is tied
14//! to it, and the reduction gets the clef that best fits its notes.
15
16use std::borrow::Cow;
17use std::sync::Arc;
18
19use crate::chord::Chord;
20use crate::clef::Clef;
21use crate::defaults::{FloatType, IntegerType};
22use crate::duration::Duration;
23use crate::error::{Error, Result};
24use crate::makenotation::op_frac;
25use crate::meter::TimeSignature;
26use crate::notation::{Tie, TieType};
27use crate::pitch::Pitch;
28use crate::stream::{Stream, StreamElement, StreamKind};
29use crate::tree::{Flatten, TimespanTree, as_timespans};
30
31/// What a reduction is asked for: music21's arguments to `ChordReducer.run`.
32#[derive(Clone, Debug)]
33#[non_exhaustive]
34pub struct ReductionOptions {
35    /// How many chords a measure may keep: music21's
36    /// `maximumNumberOfChords`, three by default.
37    pub maximum_number_of_chords: usize,
38    /// Whether every chord is written in closed position afterwards:
39    /// music21's `closedPosition`.
40    pub closed_position: bool,
41    /// The octave a chord in closed position has its lowest note in, or
42    /// its own octave with `None`: music21's `forceOctave`, 4 by default.
43    pub force_octave: Option<IntegerType>,
44    /// Chords whose interval classes count as consonant however they
45    /// sound: music21's `allowableChords`.
46    pub allowable_chords: Vec<Chord>,
47    /// Chords whose interval classes count as dissonant however they
48    /// sound: music21's `forbiddenChords`.
49    pub forbidden_chords: Vec<Chord>,
50    /// Whether the notes above the bass of a dissonant moment go before
51    /// the measures are weighed: music21's `removeDissonances`, off by
52    /// default.
53    pub remove_dissonances: bool,
54    /// What each slice of a measure weighs: music21's `weightAlgorithm`,
55    /// [`slice_weight`] by default.
56    pub weight_algorithm: SliceWeigher,
57}
58
59impl Default for ReductionOptions {
60    fn default() -> Self {
61        Self {
62            maximum_number_of_chords: 3,
63            closed_position: false,
64            allowable_chords: Vec::new(),
65            forbidden_chords: Vec::new(),
66            remove_dissonances: false,
67            force_octave: Some(4),
68            weight_algorithm: SliceWeigher::default(),
69        }
70    }
71}
72
73/// What a slice of a measure weighs, given its quarter length, the meter's
74/// accent where it starts, whether it is the last sounding slice of the
75/// measure and whether it is consonant: music21's `weightAlgorithm`.
76///
77/// ```
78/// use music21::analysis::reduce_chords::{ReductionOptions, SliceWeigher};
79///
80/// let mut options = ReductionOptions::default();
81/// options.weight_algorithm = SliceWeigher::new(|quarter_length, _, _, _| quarter_length);
82/// assert_eq!(options.weight_algorithm.weigh(2.0, 0.5, false, true), 2.0);
83/// ```
84#[derive(Clone)]
85pub struct SliceWeigher(Arc<SliceWeight>);
86
87/// The function a [`SliceWeigher`] holds.
88type SliceWeight = dyn Fn(FloatType, FloatType, bool, bool) -> FloatType + Send + Sync;
89
90impl SliceWeigher {
91    /// A weigher that calls `weigh` with a slice's quarter length, accent,
92    /// whether it is last and whether it is consonant.
93    pub fn new(
94        weigh: impl Fn(FloatType, FloatType, bool, bool) -> FloatType + Send + Sync + 'static,
95    ) -> Self {
96        Self(Arc::new(weigh))
97    }
98
99    /// What a slice weighs.
100    pub fn weigh(
101        &self,
102        quarter_length: FloatType,
103        accent: FloatType,
104        is_last: bool,
105        is_consonant: bool,
106    ) -> FloatType {
107        (self.0)(quarter_length, accent, is_last, is_consonant)
108    }
109}
110
111impl Default for SliceWeigher {
112    fn default() -> Self {
113        Self::new(slice_weight)
114    }
115}
116
117impl std::fmt::Debug for SliceWeigher {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.write_str("SliceWeigher")
120    }
121}
122
123/// What a slice weighs by default: music21's `sliceWeight`. Its quarter
124/// length times its accent, or its quarter length alone if it is the last
125/// sounding slice, times a tenth unless it is consonant.
126///
127/// ```
128/// use music21::analysis::reduce_chords::slice_weight;
129///
130/// assert_eq!(slice_weight(2.0, 0.5, false, true), 1.0);
131/// assert!((slice_weight(2.0, 0.5, true, false) - 0.2).abs() < 1e-12);
132/// ```
133pub fn slice_weight(
134    quarter_length: FloatType,
135    accent: FloatType,
136    is_last: bool,
137    is_consonant: bool,
138) -> FloatType {
139    let weight = if is_last {
140        quarter_length
141    } else {
142        quarter_length * accent
143    };
144    if is_consonant { weight } else { weight * 0.1 }
145}
146
147/// A score reduced to its chords: music21's `ChordReducer.run`, which hands
148/// back a score of one part, the first part's measures holding the chords.
149///
150/// # Errors
151///
152/// A stream that is not a score or has no part, a meter that cannot weigh
153/// a slice, or a length no duration can have.
154pub fn reduce_chords(score: &Stream, options: &ReductionOptions) -> Result<Stream> {
155    if score.kind() != StreamKind::Score {
156        return Err(Error::Analysis(
157            "a reduction is of a score, as music21's is".to_string(),
158        ));
159    }
160    let template = score
161        .parts()
162        .into_iter()
163        .next()
164        .ok_or_else(|| Error::Analysis("a score with no part to reduce".to_string()))?;
165    let mut reduction = Reduction::of(score);
166    reduction.remove_zero_duration();
167    if options.remove_dissonances {
168        reduction.remove_vertical_dissonances(options);
169    }
170    let consonance = Consonance::of(options);
171
172    let mut part = crate::stream::chordify::template_of(template);
173    let points = reduction.time_points();
174    let mut meter = TimeSignature::default();
175    let mut index = 0;
176    for event in template.events() {
177        let Some(measure) = event.element().as_stream() else {
178            continue;
179        };
180        if measure.kind() != StreamKind::Measure {
181            continue;
182        }
183        if let Some(stated) = stated_meter(measure) {
184            meter = stated.clone();
185        }
186        let start = event.offset();
187        let length = match measure.end_offset() {
188            0.0 => meter.bar_quarter_length(),
189            length => length,
190        };
191        let slices = reduction.measure_slices(start, op_frac(start + length), &points);
192        let weights = weigh_slices(
193            &slices,
194            &meter,
195            measure.padding_left(),
196            &consonance,
197            &options.weight_algorithm,
198        )?;
199        let kept = keep_heaviest_slices(&slices, &weights, options.maximum_number_of_chords, 0.25);
200        let mut made = Vec::new();
201        if kept.is_empty() {
202            made.push((
203                0.0,
204                StreamElement::Rest(crate::rest::Rest::new(Duration::new(length)?)),
205            ));
206        }
207        for slice in &kept {
208            made.push((slice.offset, reduction.element_at(slice)?));
209        }
210        if let Some(reduced) = nth_measure(&mut part, index) {
211            for (offset, element) in made {
212                reduced.insert(offset, element);
213            }
214        }
215        index += 1;
216    }
217    if options.closed_position {
218        close_positions(&mut part, options.force_octave);
219    }
220    tie_across_barlines(&mut part);
221    let pitches = part.pitches();
222    part.retain_leaves(&mut |_, element| !matches!(element, StreamElement::Clef(_)));
223    if let Some(first) = nth_measure(&mut part, 0) {
224        first.insert_sorted(vec![crate::stream::StreamEvent::new(
225            0.0,
226            Clef::best_for(&pitches, true),
227        )]);
228    }
229    let mut answer = Stream::with_kind(StreamKind::Score);
230    answer.insert(0.0, part);
231    Ok(answer)
232}
233
234/// The meter a measure states at its start: music21's
235/// `Measure.timeSignature`.
236fn stated_meter(measure: &Stream) -> Option<&TimeSignature> {
237    measure
238        .events()
239        .iter()
240        .find_map(|event| match event.element() {
241            StreamElement::TimeSignature(meter) => Some((event.offset(), meter)),
242            _ => None,
243        })
244        .filter(|(offset, _)| *offset == 0.0)
245        .map(|(_, meter)| meter)
246}
247
248/// The measure of a part counted from nought.
249fn nth_measure(part: &mut Stream, index: usize) -> Option<&mut Stream> {
250    part.events_mut()
251        .iter_mut()
252        .filter_map(|event| match event.element_mut() {
253            StreamElement::Stream(inner) if inner.kind() == StreamKind::Measure => {
254                Some(inner.as_mut())
255            }
256            _ => None,
257        })
258        .nth(index)
259}
260
261/// One stretch of one part, and the notes sounding in it: music21's
262/// `PitchedTimespan` as the reducer keeps it.
263#[derive(Clone, Debug)]
264struct Span<'a> {
265    /// Which of the score's parts it came from, in the score's order.
266    part: usize,
267    offset: FloatType,
268    end: FloatType,
269    element: Cow<'a, StreamElement>,
270}
271
272impl Span<'_> {
273    fn quarter_length(&self) -> FloatType {
274        op_frac(self.end - self.offset)
275    }
276
277    /// The notes it sounds, which are its element's: music21's `pitches`.
278    fn pitches(&self) -> Vec<Pitch> {
279        self.element.pitches()
280    }
281
282    fn lowest(&self) -> Option<Pitch> {
283        lowest_of(&self.pitches())
284    }
285}
286
287/// Whether two notes are the one note as it is written.
288fn written_alike(one: &Pitch, other: &Pitch) -> bool {
289    one.name_with_octave() == other.name_with_octave()
290        && one.written_accidental().is_some() == other.written_accidental().is_some()
291}
292
293/// The lowest note of a list, by where it sounds.
294fn lowest_of(pitches: &[Pitch]) -> Option<Pitch> {
295    pitches
296        .iter()
297        .min_by(|left, right| left.ps().total_cmp(&right.ps()))
298        .cloned()
299}
300
301/// What counts as consonant: the interval classes of music21's
302/// `allowableChords` and `forbiddenChords`, as `run` makes them.
303struct Consonance {
304    allowable: Vec<Vec<u8>>,
305    forbidden: Vec<Vec<u8>>,
306}
307
308impl Consonance {
309    fn of(options: &ReductionOptions) -> Self {
310        Self::from_chords(&options.allowable_chords, &options.forbidden_chords)
311    }
312
313    fn from_chords(allowable: &[Chord], forbidden: &[Chord]) -> Self {
314        let classes = |chords: &[Chord]| {
315            chords
316                .iter()
317                .map(|chord| interval_classes(&chord.pitches()))
318                .collect()
319        };
320        Self {
321            allowable: classes(allowable),
322            forbidden: classes(forbidden),
323        }
324    }
325
326    /// Interval-class sets as a caller writes them, each read as a set.
327    fn from_sets(allowable: &[Vec<u8>], forbidden: &[Vec<u8>]) -> Self {
328        let sets = |given: &[Vec<u8>]| {
329            given
330                .iter()
331                .map(|classes| {
332                    let mut classes = classes.clone();
333                    classes.sort_unstable();
334                    classes.dedup();
335                    classes
336                })
337                .collect()
338        };
339        Self {
340            allowable: sets(allowable),
341            forbidden: sets(forbidden),
342        }
343    }
344
345    /// Whether notes sound consonant: music21's `isConsonant`. A forbidden
346    /// chord's interval classes are dissonant and an allowable chord's
347    /// consonant; anything else is as consonant as its chord.
348    fn holds(&self, pitches: &[Pitch]) -> bool {
349        let classes = interval_classes(pitches);
350        if self.forbidden.contains(&classes) {
351            return false;
352        }
353        if self.allowable.contains(&classes) {
354            return true;
355        }
356        Chord::new(pitches).is_ok_and(|chord| chord.is_consonant())
357    }
358}
359
360/// One stretch of a measure between two places where a note starts or
361/// stops: music21's `MeasureSlice`, its verticality read as where it
362/// stands and what sounds there.
363#[derive(Clone, Debug, PartialEq)]
364#[non_exhaustive]
365pub struct MeasureSlice {
366    /// Where it starts, from the start of the measure.
367    pub offset: FloatType,
368    /// How long it lasts.
369    pub quarter_length: FloatType,
370    /// Where its verticality stands in the score, which is where it
371    /// started before a kept slice was stretched or moved.
372    pub at: FloatType,
373    /// The notes sounding at its start: the verticality's `pitchSet`.
374    pub pitches: Vec<Pitch>,
375}
376
377impl MeasureSlice {
378    /// Its sorted pitch classes, or none where nothing sounds.
379    fn key(&self) -> Option<Vec<u8>> {
380        if self.pitches.is_empty() {
381            return None;
382        }
383        Some(sorted_classes(&self.pitches))
384    }
385}
386
387/// The stretch from `start` to `end` cut at every one of `points` inside
388/// it, each slice holding what `pitches_at` says sounds where it starts.
389fn cut(
390    start: FloatType,
391    end: FloatType,
392    points: &[FloatType],
393    pitches_at: impl Fn(FloatType) -> Vec<Pitch>,
394) -> Vec<MeasureSlice> {
395    let mut cuts = vec![start];
396    cuts.extend(points.iter().copied().filter(|at| start < *at && *at < end));
397    cuts.push(end);
398    cuts.windows(2)
399        .map(|pair| MeasureSlice {
400            offset: op_frac(pair[0] - start),
401            quarter_length: op_frac(pair[1] - pair[0]),
402            at: pair[0],
403            pitches: pitches_at(pair[0]),
404        })
405        .collect()
406}
407
408/// The stretch of `tree` from `start` to `end` cut wherever a timespan
409/// starts or stops, each slice with where it starts from `start`, how
410/// long it lasts, and what sounds there: music21's `measureSlices`.
411/// `time_points` hands over the tree's `all_time_points` where many
412/// measures are cut from one tree.
413///
414/// ```
415/// use music21::analysis::reduce_chords::measure_slices;
416/// use music21::tinynotation::from_tiny_notation;
417/// use music21::tree::{Flatten, as_timespans};
418///
419/// let line = from_tiny_notation("4/4 c2 e4 g")?;
420/// let tree = as_timespans(&line, Flatten::Flat, None);
421/// let slices = measure_slices(&tree, 1.0, 4.0, None);
422/// let cuts: Vec<(f64, f64)> = slices
423///     .iter()
424///     .map(|slice| (slice.offset, slice.quarter_length))
425///     .collect();
426/// assert_eq!(cuts, [(0.0, 1.0), (1.0, 1.0), (2.0, 1.0)]);
427/// assert_eq!(slices[1].pitches[0].name_with_octave(), "E4");
428/// # Ok::<(), music21::Error>(())
429/// ```
430pub fn measure_slices(
431    tree: &TimespanTree<'_>,
432    start: FloatType,
433    end: FloatType,
434    time_points: Option<&[FloatType]>,
435) -> Vec<MeasureSlice> {
436    let points = match time_points {
437        Some(points) => Cow::Borrowed(points),
438        None => Cow::Owned(tree.all_time_points()),
439    };
440    cut(start, end, &points, |at| {
441        tree.verticality_at(at).pitch_set()
442    })
443}
444
445/// The spans of a score, and the steps that read them.
446struct Reduction<'a> {
447    spans: Vec<Span<'a>>,
448}
449
450impl<'a> Reduction<'a> {
451    /// The score read as spans, its parts in the score's own order, which
452    /// is the order music21's `allParts` answers.
453    fn of(score: &'a Stream) -> Self {
454        let parts: Vec<&Stream> = score.parts().into_iter().collect();
455        let timespans = as_timespans(
456            score,
457            Flatten::Flat,
458            Some(|element: &StreamElement| {
459                matches!(element, StreamElement::Note(_) | StreamElement::Chord(_))
460            }),
461        );
462        let mut spans = Vec::new();
463        for span in timespans.timespans() {
464            let part = span
465                .part()
466                .and_then(|held| {
467                    parts.iter().position(|known| {
468                        std::ptr::eq(std::ptr::from_ref(*known), std::ptr::from_ref(held))
469                    })
470                })
471                .unwrap_or(0);
472            spans.push(Span {
473                part,
474                offset: span.offset(),
475                end: span.end_time(),
476                element: Cow::Borrowed(span.element()),
477            });
478        }
479        Self { spans }
480    }
481
482    /// Spans of no length go: music21's `removeZeroDurationTimespans`.
483    fn remove_zero_duration(&mut self) {
484        self.spans.retain(|span| span.quarter_length() != 0.0);
485    }
486
487    /// Every offset a span starts or stops at, in order: music21's
488    /// `allTimePoints`.
489    fn time_points(&self) -> Vec<FloatType> {
490        let mut points: Vec<FloatType> = self
491            .spans
492            .iter()
493            .flat_map(|span| [span.offset, span.end])
494            .collect();
495        points.sort_by(FloatType::total_cmp);
496        points.dedup();
497        points
498    }
499
500    /// The spans starting at an offset.
501    fn starting_at(&self, offset: FloatType) -> Vec<usize> {
502        self.spans
503            .iter()
504            .enumerate()
505            .filter(|(_, span)| span.offset == offset)
506            .map(|(at, _)| at)
507            .collect()
508    }
509
510    /// The spans sounding at an offset, those starting there first and
511    /// then those that began earlier: music21's
512    /// `startAndOverlapTimespans`.
513    fn sounding_at(&self, offset: FloatType) -> Vec<usize> {
514        let mut places = self.starting_at(offset);
515        places.extend(
516            self.spans
517                .iter()
518                .enumerate()
519                .filter(|(_, span)| span.offset < offset && offset < span.end)
520                .map(|(at, _)| at),
521        );
522        places
523    }
524
525    /// The notes above the bass of a dissonant moment go: music21's
526    /// `removeVerticalDissonances`.
527    ///
528    /// music21 walks the verticalities live, from the first offset onwards
529    /// by `nextVerticality`, and takes spans out as it goes -- so what
530    /// sounds at a later offset is what the earlier removals left, and the
531    /// walk steps to the next offset a span still starts at.
532    fn remove_vertical_dissonances(&mut self, options: &ReductionOptions) {
533        let consonance = Consonance::of(options);
534        let mut offset = self.lowest_offset();
535        while let Some(at) = offset {
536            let pitches = self.pitch_set(at);
537            if !consonance.holds(&pitches)
538                && let Some(lowest) = lowest_of(&pitches)
539            {
540                let dropped: Vec<usize> = self
541                    .starting_at(at)
542                    .into_iter()
543                    .filter(|place| {
544                        self.spans[*place]
545                            .lowest()
546                            .is_some_and(|pitch| !written_alike(&pitch, &lowest))
547                    })
548                    .collect();
549                self.drop_places(&dropped);
550            }
551            offset = self.offset_after(at);
552        }
553    }
554
555    /// The first offset a span starts at.
556    fn lowest_offset(&self) -> Option<FloatType> {
557        self.spans
558            .iter()
559            .map(|span| span.offset)
560            .min_by(FloatType::total_cmp)
561    }
562
563    /// The next offset a span starts at: music21's `nextStartOffset`.
564    fn offset_after(&self, offset: FloatType) -> Option<FloatType> {
565        self.spans
566            .iter()
567            .map(|span| span.offset)
568            .filter(|at| *at > offset)
569            .min_by(FloatType::total_cmp)
570    }
571
572    /// The notes sounding at an offset, each written name once: music21's
573    /// `Verticality.pitchSet`.
574    fn pitch_set(&self, offset: FloatType) -> Vec<Pitch> {
575        let mut pitches: Vec<Pitch> = Vec::new();
576        for place in self.sounding_at(offset) {
577            for pitch in self.spans[place].pitches() {
578                if !pitches.iter().any(|known| written_alike(known, &pitch)) {
579                    pitches.push(pitch);
580                }
581            }
582        }
583        pitches.sort_by(|left, right| left.ps().total_cmp(&right.ps()));
584        pitches
585    }
586
587    /// Takes spans out by where they stand.
588    fn drop_places(&mut self, dropped: &[usize]) {
589        if dropped.is_empty() {
590            return;
591        }
592        let dropped: std::collections::HashSet<usize> = dropped.iter().copied().collect();
593        let mut at = 0;
594        self.spans.retain(|_| {
595            let kept = !dropped.contains(&at);
596            at += 1;
597            kept
598        });
599    }
600
601    /// A measure from `start` to `end` cut wherever a span starts or
602    /// stops: music21's `measureSlices`, given the score's time points.
603    fn measure_slices(
604        &self,
605        start: FloatType,
606        end: FloatType,
607        points: &[FloatType],
608    ) -> Vec<MeasureSlice> {
609        cut(start, end, points, |at| self.pitch_set(at))
610    }
611
612    /// The chord a kept slice is written as: music21's
613    /// `Verticality.makeElement` with no ties added.
614    fn element_at(&self, slice: &MeasureSlice) -> Result<StreamElement> {
615        let spans: Vec<crate::stream::chordify::Span<'_>> = self
616            .sounding_at(slice.at)
617            .into_iter()
618            .map(|place| crate::stream::chordify::Span {
619                start: self.spans[place].offset,
620                end: self.spans[place].end,
621                element: &self.spans[place].element,
622                part: self.spans[place].part,
623            })
624            .collect();
625        let options = crate::stream::ChordifyOptions {
626            add_ties: false,
627            ..crate::stream::ChordifyOptions::default()
628        };
629        crate::stream::chordify::make_element(&spans, slice.at, slice.quarter_length, &options)
630    }
631}
632
633/// What each set of sorted pitch classes sounding among `slices` weighs,
634/// in the order they are met: music21's `sliceWeights`.
635///
636/// Each slice is weighed by `weigher`, given its length, the meter's accent
637/// where it starts -- read past the measure's `padding_left`, so that a
638/// pickup is accented as the end of a bar -- whether it is the last that
639/// sounds, and whether it is consonant as [`is_consonant`] reads it with
640/// the `allowable` and `forbidden` interval-class sets.
641///
642/// # Errors
643///
644/// A meter that cannot weigh where a slice starts.
645pub fn slice_weights(
646    slices: &[MeasureSlice],
647    meter: &TimeSignature,
648    padding_left: FloatType,
649    weigher: &SliceWeigher,
650    allowable: &[Vec<u8>],
651    forbidden: &[Vec<u8>],
652) -> Result<Vec<(Vec<u8>, FloatType)>> {
653    let consonance = Consonance::from_sets(allowable, forbidden);
654    weigh_slices(slices, meter, padding_left, &consonance, weigher)
655}
656
657/// Whether notes sound consonant: music21's `isConsonant`. Notes whose
658/// [`interval_class_set`] is one of the `forbidden` sets are dissonant,
659/// and those whose set is one of the `allowable` consonant; any others are
660/// as consonant as their chord is. Each set is read as a set, in any
661/// order.
662///
663/// ```
664/// use music21::Pitch;
665/// use music21::analysis::reduce_chords::is_consonant;
666///
667/// let pitches = |names: &[&str]| -> Vec<Pitch> {
668///     names.iter().map(|name| Pitch::from_name(*name).unwrap()).collect()
669/// };
670/// assert!(is_consonant(&pitches(&["C4", "E4", "G4"]), &[], &[]));
671/// assert!(!is_consonant(&pitches(&["C4", "D4"]), &[], &[]));
672/// // A major second allowed.
673/// assert!(is_consonant(&pitches(&["C4", "D4"]), &[vec![2]], &[]));
674/// ```
675pub fn is_consonant(pitches: &[Pitch], allowable: &[Vec<u8>], forbidden: &[Vec<u8>]) -> bool {
676    Consonance::from_sets(allowable, forbidden).holds(pitches)
677}
678
679/// Every interval class between two of the notes, 1 to 6, each once and
680/// in order: music21's `_getIntervalClassSet`, which is how
681/// [`is_consonant`] reads the notes and the chords a reduction allows or
682/// forbids.
683pub fn interval_class_set(pitches: &[Pitch]) -> Vec<u8> {
684    interval_classes(pitches)
685}
686
687/// [`slice_weights`], with what is consonant already read.
688fn weigh_slices(
689    slices: &[MeasureSlice],
690    meter: &TimeSignature,
691    padding: FloatType,
692    consonance: &Consonance,
693    weigher: &SliceWeigher,
694) -> Result<Vec<(Vec<u8>, FloatType)>> {
695    let sounding: Vec<usize> = (0..slices.len())
696        .filter(|at| !slices[*at].pitches.is_empty())
697        .collect();
698    let mut weights: Vec<(Vec<u8>, FloatType)> = Vec::new();
699    for at in &sounding {
700        let slice = &slices[*at];
701        let accent = meter.accent_weight_with(op_frac(slice.offset + padding), true, true)?;
702        let weight = weigher.weigh(
703            slice.quarter_length,
704            accent,
705            Some(at) == sounding.last(),
706            consonance.holds(&slice.pitches),
707        );
708        let classes = sorted_classes(&slice.pitches);
709        match weights.iter_mut().find(|(known, _)| *known == classes) {
710            Some((_, total)) => *total += weight,
711            None => weights.push((classes, weight)),
712        }
713    }
714    Ok(weights)
715}
716
717/// The slices to keep of a measure's, lengthened: music21's
718/// `keepHeaviestSlices`.
719///
720/// The `maximum` heaviest sets of classes in `weights` are kept that weigh
721/// at least `trim_below` times the heaviest. A kept slice lasts until the
722/// next kept slice of other classes, the first starting the measure and
723/// the last lasting to its end; one starting a sixteenth, thirty-second,
724/// sixty-fourth or triplet eighth past a beat is moved onto the beat.
725pub fn keep_heaviest_slices(
726    slices: &[MeasureSlice],
727    weights: &[(Vec<u8>, FloatType)],
728    maximum: usize,
729    trim_below: FloatType,
730) -> Vec<MeasureSlice> {
731    let mut sorted: Vec<&(Vec<u8>, FloatType)> = weights.iter().collect();
732    sorted.sort_by(|left, right| right.1.total_cmp(&left.1));
733    sorted.truncate(maximum);
734    let Some(heaviest) = sorted.first().map(|(_, weight)| *weight) else {
735        return Vec::new();
736    };
737    let kept_keys: Vec<&Vec<u8>> = sorted
738        .iter()
739        .filter(|(_, weight)| *weight >= heaviest * trim_below)
740        .map(|(classes, _)| classes)
741        .collect();
742
743    let mut kept: Vec<MeasureSlice> = Vec::new();
744    let mut current: Option<Vec<u8>> = None;
745    for slice in slices {
746        let key = slice.key();
747        let end = op_frac(slice.offset + slice.quarter_length);
748        if let Some(classes) = &key
749            && kept_keys.contains(&classes)
750            && key != current
751        {
752            let offset = if kept.is_empty() { 0.0 } else { slice.offset };
753            kept.push(MeasureSlice {
754                offset,
755                quarter_length: op_frac(end - offset),
756                ..slice.clone()
757            });
758            current = key;
759        } else if let Some(last) = kept.last_mut() {
760            last.quarter_length = op_frac(end - last.offset);
761        }
762    }
763    for index in 1..kept.len() {
764        let past = op_frac(kept[index].offset - kept[index].offset.trunc());
765        let rounded = (past * 1000.0).round() / 1000.0;
766        if ![0.25, 0.125, 0.333, 0.063, 0.062].contains(&rounded) {
767            continue;
768        }
769        kept[index - 1].quarter_length = op_frac(kept[index - 1].quarter_length - past);
770        kept[index].offset = op_frac(kept[index].offset - past);
771        kept[index].quarter_length = op_frac(kept[index].quarter_length + past);
772    }
773    kept.retain(|slice| slice.quarter_length != 0.0);
774    kept
775}
776
777/// The distinct pitch classes of some notes, in order.
778fn sorted_classes(pitches: &[Pitch]) -> Vec<u8> {
779    let mut classes: Vec<u8> = pitches.iter().map(pitch_class).collect();
780    classes.sort_unstable();
781    classes.dedup();
782    classes
783}
784
785/// A note's pitch class: music21's `pitchClass`.
786fn pitch_class(pitch: &Pitch) -> u8 {
787    (pitch.ps().round_ties_even() as i64).rem_euclid(12) as u8
788}
789
790/// Every interval class a list of notes holds, 1 to 6: music21's
791/// `_getIntervalClassSet`.
792fn interval_classes(pitches: &[Pitch]) -> Vec<u8> {
793    let mut classes: Vec<u8> = Vec::new();
794    for (index, one) in pitches.iter().enumerate() {
795        for other in &pitches[index + 1..] {
796            let mut semitones = (one.ps() - other.ps()).abs() as i64 % 12;
797            if semitones >= 6 {
798                semitones = 12 - semitones;
799            }
800            let class = semitones as u8;
801            if class != 0 && !classes.contains(&class) {
802                classes.push(class);
803            }
804        }
805    }
806    classes.sort_unstable();
807    classes
808}
809
810/// How much a chord weighs in its measure: music21's four weight
811/// algorithms, one of which its `ChordReducer` hands to
812/// `computeMeasureChordWeights`.
813#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
814#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
815pub enum ChordWeight {
816    /// How long the chord sounds, and nothing else: music21's
817    /// `quarterLengthOnly`, which is what `computeMeasureChordWeights`
818    /// weighs by when it is handed none.
819    #[default]
820    Length,
821    /// That times how strong its beat is: `quarterLengthBeatStrength`.
822    LengthByBeat,
823    /// The same, except that the last chord of the measure counts as a
824    /// full beat: `quarterLengthBeatStrengthMeasurePosition`.
825    LengthByBeatInMeasure,
826    /// The same again, a tenth of it where the chord is dissonant:
827    /// `qlbsmpConsonance`, which is the one a reduction weighs by.
828    Consonance,
829}
830
831impl ChordWeight {
832    /// The name music21 gives the method.
833    #[must_use]
834    pub fn name(self) -> &'static str {
835        match self {
836            Self::Length => "quarterLengthOnly",
837            Self::LengthByBeat => "quarterLengthBeatStrength",
838            Self::LengthByBeatInMeasure => "quarterLengthBeatStrengthMeasurePosition",
839            Self::Consonance => "qlbsmpConsonance",
840        }
841    }
842
843    /// What a name stands for, which is how a caller handing music21's own
844    /// method over is read.
845    #[must_use]
846    pub fn named(name: &str) -> Option<Self> {
847        [
848            Self::Length,
849            Self::LengthByBeat,
850            Self::LengthByBeatInMeasure,
851            Self::Consonance,
852        ]
853        .into_iter()
854        .find(|found| found.name() == name)
855    }
856
857    /// How much one chord weighs under this algorithm.
858    #[must_use]
859    pub fn of(
860        self,
861        chord: &Chord,
862        length: FloatType,
863        beat_strength: FloatType,
864        last_in_measure: bool,
865    ) -> FloatType {
866        let by_beat = if last_in_measure {
867            length
868        } else {
869            length * beat_strength
870        };
871        match self {
872            Self::Length => length,
873            Self::LengthByBeat => length * beat_strength,
874            Self::LengthByBeatInMeasure => by_beat,
875            Self::Consonance => {
876                let consonance = if chord.is_consonant() { 1.0 } else { 0.1 };
877                by_beat * consonance
878            }
879        }
880    }
881}
882
883/// One chord of a measure as the weighing reads it: where it stands, what
884/// it is, and how strong the beat it stands on is.
885///
886/// music21 reads the beat strength off the chord's own place in the measure
887/// holding it; a chord here knows neither, so the caller says.
888#[derive(Clone, Copy, Debug)]
889pub struct WeighedChord<'a> {
890    /// Where the chord stands in the measure.
891    pub offset: FloatType,
892    /// The chord itself.
893    pub chord: &'a Chord,
894    /// How strong the beat it stands on is.
895    pub beat_strength: FloatType,
896}
897
898/// How much each chord of a measure weighs, keyed by the pitch classes it
899/// sounds and in the order the chords were met: music21's
900/// `computeMeasureChordWeights`.
901///
902/// Two chords of the same classes weigh as one, which is what makes a chord
903/// struck again over a moving bass count towards the chord already sounding
904/// rather than as a chord of its own. The classes are sorted, so two
905/// voicings of them are the one chord.
906#[must_use]
907pub fn measure_chord_weights(
908    chords: &[WeighedChord<'_>],
909    algorithm: ChordWeight,
910) -> Vec<(Vec<u8>, FloatType)> {
911    let mut weights: Vec<(Vec<u8>, FloatType)> = Vec::new();
912    for (index, held) in chords.iter().enumerate() {
913        let classes = pitch_classes(held.chord);
914        let length = held
915            .chord
916            .duration()
917            .map_or(0.0, crate::duration::Duration::quarter_length);
918        let weight = algorithm.of(
919            held.chord,
920            length,
921            held.beat_strength,
922            index + 1 == chords.len(),
923        );
924        match weights.iter_mut().find(|(known, _)| *known == classes) {
925            Some((_, total)) => *total += weight,
926            None => weights.push((classes, weight)),
927        }
928    }
929    weights
930}
931
932/// One chord a reduction keeps: which of the measure's chords it is, where
933/// it goes and how long it sounds.
934#[derive(Clone, Copy, Debug, PartialEq)]
935pub struct KeptChord {
936    /// Which of the chords given it is, counted from nought.
937    pub at: usize,
938    /// Where it stands in the measure afterwards.
939    pub offset: FloatType,
940    /// How long it sounds afterwards.
941    pub length: FloatType,
942}
943
944/// Which chords of a measure a reduction keeps, and where: music21's
945/// `reduceMeasureToNChords` read as places rather than as a stream.
946///
947/// Every chord not named is one the reduction drops, and a kept chord takes
948/// the time of the chords it stands for -- music21's greedy walk. `others`
949/// is where the measure's other elements stand, which the evening out of
950/// syncopations reads: a chord may take its time from one of those, which
951/// changes nothing a score says since none of them sounds.
952#[must_use]
953pub fn reduced_measure_places(
954    chords: &[WeighedChord<'_>],
955    others: &[FloatType],
956    maximum: usize,
957    trim_below: FloatType,
958    algorithm: ChordWeight,
959) -> Vec<KeptChord> {
960    let weights = measure_chord_weights(chords, algorithm);
961    if weights.is_empty() {
962        return Vec::new();
963    }
964    let mut sorted = weights;
965    sorted.sort_by(|(_, left), (_, right)| right.total_cmp(left));
966    let keep = maximum.min(sorted.len());
967    let highest = sorted[0].1;
968    let kept: Vec<Vec<u8>> = sorted
969        .into_iter()
970        .take(keep)
971        .take_while(|(_, weight)| *weight >= highest * trim_below)
972        .map(|(classes, _)| classes)
973        .collect();
974
975    let mut greedy: Option<usize> = None;
976    let mut written: Vec<KeptChord> = Vec::new();
977    let mut running = 0.0;
978    let mut last_classes: Option<Vec<u8>> = None;
979    for (at, held) in chords.iter().enumerate() {
980        let classes = pitch_classes(held.chord);
981        let length = held
982            .chord
983            .duration()
984            .map_or(0.0, crate::duration::Duration::quarter_length);
985        if kept.contains(&classes) && Some(&classes) != last_classes.as_ref() {
986            match greedy {
987                None if held.offset != 0.0 => running = held.offset,
988                None => {}
989                Some(place) => {
990                    written[place].length = running;
991                    running = 0.0;
992                }
993            }
994            let offset = if greedy.is_none() && held.offset != 0.0 {
995                0.0
996            } else {
997                held.offset
998            };
999            written.push(KeptChord {
1000                at,
1001                offset,
1002                length: 0.0,
1003            });
1004            greedy = Some(written.len() - 1);
1005            last_classes = Some(classes);
1006            running = op_frac(running + length);
1007        } else {
1008            running = op_frac(running + length);
1009        }
1010    }
1011    if let Some(place) = greedy {
1012        written[place].length = running;
1013    }
1014    even_out_syncopations(&mut written, others);
1015    written
1016}
1017
1018/// A measure reduced to the few chords weighing most in it: music21's
1019/// `reduceMeasureToNChords`, built on [`reduced_measure_places`].
1020///
1021/// `meter` is the meter in force where the measure stands, which the beat
1022/// strengths are read in; without one every beat is as strong as every
1023/// other, as music21 reads a measure standing in no meter.
1024///
1025/// # Errors
1026///
1027/// A length no duration can have.
1028pub fn reduce_measure_to_n_chords(
1029    measure: &Stream,
1030    maximum: usize,
1031    trim_below: FloatType,
1032    meter: Option<TimeSignature>,
1033    algorithm: ChordWeight,
1034) -> Result<Stream> {
1035    let held: Vec<(FloatType, Chord)> = measure
1036        .events()
1037        .iter()
1038        .filter_map(|event| match event.element() {
1039            StreamElement::Chord(chord) => Some((event.offset(), chord.clone())),
1040            _ => None,
1041        })
1042        .collect();
1043    // The measure as it stands without its notes, which is what music21
1044    // leaves when it removes the chords it does not keep: the clef, the
1045    // key, the meter, a layout, a barline and any rest the chordifying
1046    // made stay where they stood.
1047    let mut answer = measure.with_events(
1048        measure
1049            .events()
1050            .iter()
1051            .filter(|event| !matches!(event.element(), StreamElement::Chord(_)))
1052            .cloned()
1053            .collect(),
1054    );
1055    let others: Vec<FloatType> = answer
1056        .events()
1057        .iter()
1058        .map(crate::stream::StreamEvent::offset)
1059        .collect();
1060    let chords: Vec<WeighedChord<'_>> = held
1061        .iter()
1062        .map(|(offset, chord)| WeighedChord {
1063            offset: *offset,
1064            chord,
1065            beat_strength: match &meter {
1066                Some(meter) => meter.accent_weight(*offset).unwrap_or(1.0),
1067                None => 1.0,
1068            },
1069        })
1070        .collect();
1071    let kept = reduced_measure_places(&chords, &others, maximum, trim_below, algorithm);
1072    if kept.is_empty() {
1073        let length = measure.end_offset();
1074        answer.insert(0.0, crate::rest::Rest::new(Duration::new(length)?));
1075        return Ok(answer);
1076    }
1077    for place in kept {
1078        let mut chord = held[place.at].1.clone();
1079        undecide_accidentals(&mut chord);
1080        chord.set_duration(Duration::new(place.length.max(0.0))?);
1081        answer.insert(place.offset, chord);
1082    }
1083    Ok(answer)
1084}
1085
1086/// The pitch classes a chord holds, sorted, as music21 keys its weights
1087/// by them: two chords of the same classes in any order weigh as one.
1088fn pitch_classes(chord: &Chord) -> Vec<u8> {
1089    sorted_classes(&chord.pitches())
1090}
1091
1092/// A chord's accidentals left undecided, as music21 leaves a kept chord's:
1093/// it clears the display status of every accidental actually written.
1094fn undecide_accidentals(chord: &mut Chord) {
1095    for note in chord.notes_mut() {
1096        let mut pitch = note.pitch().clone();
1097        if let Some(accidental) = pitch.written_accidental_mut() {
1098            accidental.set_display_status(None);
1099        }
1100        note.set_pitch(pitch);
1101    }
1102}
1103
1104/// A chord standing a quarter, an eighth or a third of the way through a
1105/// beat is moved onto it, the element before it giving up the difference:
1106/// music21's "even chord lengths" pass, which walks the measure in order
1107/// and reads each offset as it has been left by the one before.
1108fn even_out_syncopations(written: &mut [KeptChord], others: &[FloatType]) {
1109    for index in 0..written.len() {
1110        let offset = written[index].offset;
1111        let whole = offset.trunc();
1112        let past = offset - whole;
1113        let rounded = (past * 1000.0).round() / 1000.0;
1114        if ![0.250, 0.125, 0.333, 0.063, 0.062].contains(&rounded) {
1115            continue;
1116        }
1117        // The element standing before it, which is a chord only where no
1118        // other element of the measure stands between the two.
1119        let before = written[..index].last().map(|kept| kept.offset);
1120        let Some(before) = before else {
1121            // music21 leaves the first element of a measure alone.
1122            if index == 0 && !others.iter().any(|at| *at < offset) {
1123                continue;
1124            }
1125            written[index].offset = whole;
1126            written[index].length = op_frac(written[index].length + past);
1127            continue;
1128        };
1129        let interrupted = others.iter().any(|at| *at > before && *at <= offset);
1130        if !interrupted {
1131            written[index - 1].length = op_frac(written[index - 1].length - past);
1132        }
1133        written[index].offset = whole;
1134        written[index].length = op_frac(written[index].length + past);
1135    }
1136}
1137
1138/// Every chord of the part written in closed position with its lowest
1139/// note in `force_octave`: music21's `closedPosition(forceOctave=...,
1140/// inPlace=True)` over every chord of the reduction.
1141fn close_positions(stream: &mut Stream, force_octave: Option<IntegerType>) {
1142    for event in stream.events_mut() {
1143        match event.element_mut() {
1144            StreamElement::Stream(inner) => close_positions(inner, force_octave),
1145            StreamElement::Chord(chord) => *chord = chord.closed_position(force_octave, false),
1146            _ => {}
1147        }
1148    }
1149}
1150
1151/// The last note or chord of each measure tied to the first of the next
1152/// when they sound the same pitches, a measure with nothing in it passed
1153/// over.
1154fn tie_across_barlines(part: &mut Stream) {
1155    let mut previous: Option<(usize, usize)> = None;
1156    for at in 0..part.events().len() {
1157        let Some(measure) = part.events()[at].element().as_stream() else {
1158            continue;
1159        };
1160        if measure.kind() != StreamKind::Measure {
1161            continue;
1162        }
1163        let played: Vec<usize> = (0..measure.events().len())
1164            .filter(|place| {
1165                matches!(
1166                    measure.events()[*place].element(),
1167                    StreamElement::Note(_) | StreamElement::Chord(_) | StreamElement::Rest(_)
1168                )
1169            })
1170            .collect();
1171        let (Some(&first), Some(&last)) = (played.first(), played.last()) else {
1172            continue;
1173        };
1174        if let Some((before, place)) = previous {
1175            let pitches_before = sounded(part, before, place);
1176            if pitches_before.is_some() && pitches_before == sounded(part, at, first) {
1177                add_tie(element_of(part, before, place), TieType::Start);
1178                add_tie(element_of(part, at, first), TieType::Stop);
1179            }
1180        }
1181        previous = Some((at, last));
1182    }
1183}
1184
1185/// The pitches of a note or chord in a measure of the part, or none for a
1186/// rest.
1187fn sounded(part: &Stream, measure: usize, place: usize) -> Option<Vec<Pitch>> {
1188    let element = part.events()[measure].element().as_stream()?.events()[place].element();
1189    match element {
1190        StreamElement::Note(_) | StreamElement::Chord(_) => Some(element.pitches()),
1191        _ => None,
1192    }
1193}
1194
1195/// An element of a measure of the part, to be changed.
1196fn element_of(part: &mut Stream, measure: usize, place: usize) -> &mut StreamElement {
1197    match part.events_mut()[measure].element_mut() {
1198        StreamElement::Stream(inner) => inner.events_mut()[place].element_mut(),
1199        element => element,
1200    }
1201}
1202
1203/// A tie of `tie_type` put on a note or chord, which becomes a continuing
1204/// tie where it is already tied the other way: music21's `_addTie`.
1205fn add_tie(element: &mut StreamElement, tie_type: TieType) {
1206    let existing = match element {
1207        StreamElement::Note(note) => note.tie().map(Tie::tie_type),
1208        StreamElement::Chord(chord) => chord.tie().map(Tie::tie_type),
1209        _ => return,
1210    };
1211    let tie_type = match existing {
1212        Some(known) if known != tie_type => TieType::Continue,
1213        _ => tie_type,
1214    };
1215    let tie = Some(Tie::new(tie_type));
1216    match element {
1217        StreamElement::Note(note) => note.set_tie(tie),
1218        StreamElement::Chord(chord) => chord.set_tie(tie),
1219        _ => {}
1220    }
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225    use super::*;
1226    use crate::tinynotation::from_tiny_notation;
1227
1228    fn score_of(lines: &[&str]) -> Stream {
1229        let mut score = Stream::with_kind(StreamKind::Score);
1230        for line in lines {
1231            score.insert(0.0, from_tiny_notation(line).unwrap());
1232        }
1233        score
1234    }
1235
1236    /// Each measure of the reduction as its chords: offset, length and
1237    /// the notes.
1238    fn chords_of(reduction: &Stream) -> Vec<Vec<(FloatType, FloatType, Vec<String>)>> {
1239        let part = reduction.parts().into_iter().next().unwrap();
1240        part.measures()
1241            .into_iter()
1242            .map(|measure| {
1243                measure
1244                    .events()
1245                    .iter()
1246                    .filter_map(|event| match event.element() {
1247                        StreamElement::Chord(chord) => Some((
1248                            event.offset(),
1249                            chord.duration().map_or(0.0, Duration::quarter_length),
1250                            chord
1251                                .pitches()
1252                                .iter()
1253                                .map(Pitch::name_with_octave)
1254                                .collect(),
1255                        )),
1256                        _ => None,
1257                    })
1258                    .collect()
1259            })
1260            .collect()
1261    }
1262
1263    /// music21's `testMeasureStream1`: a measure of C major, a dissonant
1264    /// chord on the third beat, and C major again.
1265    fn test_measure() -> Stream {
1266        let mut measure = Stream::with_kind(StreamKind::Measure);
1267        measure.insert(0.0, TimeSignature::new(4, 4).unwrap());
1268        for (offset, notes, length) in [
1269            (0.0, "C4 E4 G4 C5", 2.0),
1270            (2.0, "C4 E4 F4 B4", 1.0),
1271            (3.0, "C4 E4 G4 C5", 1.0),
1272        ] {
1273            let chord = Chord::new(notes)
1274                .unwrap()
1275                .with_duration(Duration::new(length).unwrap());
1276            measure.insert(offset, chord);
1277        }
1278        measure
1279    }
1280
1281    fn chords_only(element: &StreamElement) -> bool {
1282        matches!(element, StreamElement::Note(_) | StreamElement::Chord(_))
1283    }
1284
1285    #[test]
1286    fn a_measure_is_cut_where_its_chords_change() {
1287        let measure = test_measure();
1288        let tree = as_timespans(&measure, Flatten::Flat, Some(chords_only));
1289        let slices = measure_slices(&tree, 0.0, 4.0, None);
1290        let cuts: Vec<(FloatType, FloatType, usize)> = slices
1291            .iter()
1292            .map(|slice| (slice.offset, slice.quarter_length, slice.pitches.len()))
1293            .collect();
1294        assert_eq!(cuts, [(0.0, 2.0, 4), (2.0, 1.0, 4), (3.0, 1.0, 4)]);
1295        // Read from the middle, offsets count from where it was asked.
1296        let later = measure_slices(&tree, 1.0, 4.0, Some(&tree.all_time_points()));
1297        assert_eq!(later[0].offset, 0.0);
1298        assert_eq!(later[0].at, 1.0);
1299        assert_eq!(later[0].quarter_length, 1.0);
1300    }
1301
1302    #[test]
1303    fn slices_are_weighed_by_length_beat_and_consonance() {
1304        let measure = test_measure();
1305        let tree = as_timespans(&measure, Flatten::Flat, Some(chords_only));
1306        let slices = measure_slices(&tree, 0.0, 4.0, None);
1307        let meter = TimeSignature::new(4, 4).unwrap();
1308        let weights =
1309            slice_weights(&slices, &meter, 0.0, &SliceWeigher::default(), &[], &[]).unwrap();
1310        assert_eq!(weights.len(), 2);
1311        assert_eq!(weights[0].0, [0, 4, 7]);
1312        assert!((weights[0].1 - 3.0).abs() < 1e-9);
1313        assert_eq!(weights[1].0, [0, 4, 5, 11]);
1314        assert!((weights[1].1 - 0.05).abs() < 1e-9);
1315
1316        let by_length = SliceWeigher::new(|quarter_length, _, _, _| quarter_length);
1317        let weights = slice_weights(&slices, &meter, 0.0, &by_length, &[], &[]).unwrap();
1318        assert_eq!(weights[1].1, 1.0);
1319
1320        // A chord allowed is consonant, and so weighs in full.
1321        let allowed = interval_class_set(&Chord::new("C4 E4 F4 B4").unwrap().pitches());
1322        let weights = slice_weights(
1323            &slices,
1324            &meter,
1325            0.0,
1326            &SliceWeigher::default(),
1327            &[allowed],
1328            &[],
1329        )
1330        .unwrap();
1331        assert!((weights[1].1 - 0.5).abs() < 1e-9);
1332    }
1333
1334    #[test]
1335    fn the_heaviest_slices_are_kept_and_lengthened() {
1336        let measure = test_measure();
1337        let tree = as_timespans(&measure, Flatten::Flat, Some(chords_only));
1338        let slices = measure_slices(&tree, 0.0, 4.0, None);
1339        let weights = vec![(vec![0, 4, 7], 3.0), (vec![0, 4, 5, 11], 1.0)];
1340        let kept: Vec<(FloatType, FloatType)> = keep_heaviest_slices(&slices, &weights, 3, 0.25)
1341            .iter()
1342            .map(|slice| (slice.offset, slice.quarter_length))
1343            .collect();
1344        assert_eq!(kept, [(0.0, 2.0), (2.0, 1.0), (3.0, 1.0)]);
1345        // With a higher floor the lighter chord goes, and the first lasts
1346        // through it.
1347        let kept = keep_heaviest_slices(&slices, &weights, 3, 0.5);
1348        assert_eq!(kept.len(), 1);
1349        assert_eq!(kept[0].quarter_length, 4.0);
1350        assert!(keep_heaviest_slices(&slices, &[], 3, 0.25).is_empty());
1351    }
1352
1353    #[test]
1354    fn a_forbidden_chord_is_dissonant() {
1355        let triad = Chord::new("C4 E4 G4").unwrap().pitches();
1356        assert!(is_consonant(&triad, &[], &[]));
1357        assert_eq!(interval_class_set(&triad), [3, 4, 5]);
1358        // Read as a set, whatever order it is written in.
1359        assert!(!is_consonant(&triad, &[], &[vec![5, 3, 4, 4]]));
1360    }
1361
1362    #[test]
1363    fn a_passing_dissonance_is_dropped() {
1364        let score = score_of(&["4/4 e2 f4 e4 g2 r2", "4/4 C1 C2 r2"]);
1365        let reduction = reduce_chords(&score, &ReductionOptions::default()).unwrap();
1366        let names = |names: &[&str]| names.iter().map(ToString::to_string).collect();
1367        assert_eq!(
1368            chords_of(&reduction),
1369            [
1370                vec![(0.0, 4.0, names(&["C3", "E4"]))],
1371                vec![(0.0, 4.0, names(&["C3", "G4"]))],
1372            ]
1373        );
1374    }
1375
1376    #[test]
1377    fn a_bass_gap_is_filled_up_to_the_next_span() {
1378        let score = score_of(&[
1379            "3/4 r8 r16 d'16 e'16 r16 r8 r4",
1380            "3/4 g8 G4 r8 r4",
1381            "3/4 G8 r8 r4 r16 G16 r8",
1382        ]);
1383        let reduction = reduce_chords(&score, &ReductionOptions::default()).unwrap();
1384        let names = |names: &[&str]| names.iter().map(ToString::to_string).collect();
1385        assert_eq!(
1386            chords_of(&reduction),
1387            [vec![(0.0, 3.0, names(&["G3", "G4"]))]]
1388        );
1389    }
1390
1391    #[test]
1392    fn chords_are_chosen_from_slices_of_the_score() {
1393        let score = score_of(&["4/4 c4 d4 e4 f4 g1", "4/4 C2 G,2 C1"]);
1394        let reduction = reduce_chords(&score, &ReductionOptions::default()).unwrap();
1395        let names = |names: &[&str]| names.iter().map(ToString::to_string).collect();
1396        assert_eq!(
1397            chords_of(&reduction),
1398            [
1399                vec![
1400                    (0.0, 2.0, names(&["C3", "C4"])),
1401                    (2.0, 2.0, names(&["C3", "E4"])),
1402                ],
1403                vec![(0.0, 2.0, names(&["C3", "G4"])), (2.0, 2.0, names(&["G4"]))],
1404            ]
1405        );
1406        let score = score_of(&["4/4 g4 a8 b8 c'2", "4/4 e4 f4 e2", "4/4 C1"]);
1407        let reduction = reduce_chords(&score, &ReductionOptions::default()).unwrap();
1408        assert_eq!(
1409            chords_of(&reduction),
1410            [vec![
1411                (0.0, 2.0, names(&["C3", "E4", "G4"])),
1412                (2.0, 2.0, names(&["C3", "E4", "C5"])),
1413            ]]
1414        );
1415    }
1416
1417    #[test]
1418    fn chords_are_tied_across_barlines_under_a_fitting_clef() {
1419        let score = score_of(&["4/4 e1 e1 e1 f1", "4/4 C1 C1 C1 C1"]);
1420        let reduction = reduce_chords(&score, &ReductionOptions::default()).unwrap();
1421        let part = reduction.parts().into_iter().next().unwrap();
1422        let ties: Vec<Vec<Option<TieType>>> = part
1423            .measures()
1424            .into_iter()
1425            .map(|measure| {
1426                measure
1427                    .events()
1428                    .iter()
1429                    .filter_map(|event| match event.element() {
1430                        StreamElement::Chord(chord) => Some(chord.tie().map(Tie::tie_type)),
1431                        _ => None,
1432                    })
1433                    .collect()
1434            })
1435            .collect();
1436        assert_eq!(
1437            ties,
1438            [
1439                vec![Some(TieType::Start)],
1440                vec![Some(TieType::Continue)],
1441                vec![Some(TieType::Stop)],
1442                vec![None],
1443            ]
1444        );
1445        let clefs: Vec<crate::clef::ClefKind> = part
1446            .measures()
1447            .into_iter()
1448            .flat_map(|measure| {
1449                measure
1450                    .events()
1451                    .iter()
1452                    .filter_map(|event| match event.element() {
1453                        StreamElement::Clef(clef) => Some(clef.kind()),
1454                        _ => None,
1455                    })
1456                    .collect::<Vec<_>>()
1457            })
1458            .collect();
1459        assert_eq!(clefs, [crate::clef::ClefKind::BassClef]);
1460    }
1461
1462    #[test]
1463    fn grace_notes_are_left_out() {
1464        let mut score = score_of(&["4/4 e1", "4/4 C1"]);
1465        let grace = crate::note::Note::from_name("F#4")
1466            .unwrap()
1467            .with_duration(Duration::default().grace_duration());
1468        if let StreamElement::Stream(part) = score.events_mut()[0].element_mut()
1469            && let Some(measure) = nth_measure(part, 0)
1470        {
1471            measure.insert(0.0, grace);
1472        }
1473        let reduction = reduce_chords(&score, &ReductionOptions::default()).unwrap();
1474        let names = |names: &[&str]| names.iter().map(ToString::to_string).collect();
1475        assert_eq!(
1476            chords_of(&reduction),
1477            [vec![(0.0, 4.0, names(&["C3", "E4"]))]]
1478        );
1479    }
1480
1481    #[test]
1482    fn closed_position_puts_the_lowest_note_in_the_octave_asked_for() {
1483        let score = score_of(&["4/4 e1", "4/4 C1"]);
1484        let mut options = ReductionOptions {
1485            closed_position: true,
1486            ..ReductionOptions::default()
1487        };
1488        let names = |names: &[&str]| names.iter().map(ToString::to_string).collect();
1489        let reduction = reduce_chords(&score, &options).unwrap();
1490        assert_eq!(
1491            chords_of(&reduction),
1492            [vec![(0.0, 4.0, names(&["C4", "E4"]))]]
1493        );
1494        options.force_octave = None;
1495        let reduction = reduce_chords(&score, &options).unwrap();
1496        assert_eq!(
1497            chords_of(&reduction),
1498            [vec![(0.0, 4.0, names(&["C3", "E3"]))]]
1499        );
1500    }
1501
1502    #[test]
1503    fn the_first_parts_measures_are_reduced() {
1504        let score = score_of(&["4/4 e1 f1", "2/4 C2 D2 C2 D2"]);
1505        let reduction = reduce_chords(&score, &ReductionOptions::default()).unwrap();
1506        let part = reduction.parts().into_iter().next().unwrap();
1507        let offsets: Vec<FloatType> = part
1508            .events()
1509            .iter()
1510            .filter(|event| {
1511                event
1512                    .element()
1513                    .as_stream()
1514                    .is_some_and(|inner| inner.kind() == StreamKind::Measure)
1515            })
1516            .map(crate::stream::StreamEvent::offset)
1517            .collect();
1518        assert_eq!(offsets, [0.0, 4.0]);
1519    }
1520
1521    #[test]
1522    fn pitch_classes_in_any_order_weigh_as_one() {
1523        let half = || Duration::half();
1524        let first = Chord::new("B3 D#4").unwrap().with_duration(half());
1525        let second = Chord::new("D#3 B3").unwrap().with_duration(half());
1526        let chords = [
1527            WeighedChord {
1528                offset: 0.0,
1529                chord: &first,
1530                beat_strength: 1.0,
1531            },
1532            WeighedChord {
1533                offset: 2.0,
1534                chord: &second,
1535                beat_strength: 0.5,
1536            },
1537        ];
1538        assert_eq!(
1539            measure_chord_weights(&chords, ChordWeight::Length),
1540            [(vec![3, 11], 4.0)]
1541        );
1542    }
1543}