Skip to main content

music21_rs/analysis/
reduction.rs

1//! Reductions written into a score by its lyrics: music21's
2//! `analysis.reduction`.
3//!
4//! A lyric starting `::` marks its note for a reduction, saying how it is
5//! written there: `::/p:e/o:5/nf:no/ta:3/g:Ursatz` takes the E of a chord,
6//! puts it in the fifth octave with a hollow notehead and the text `3`
7//! above, in the part for the group `Ursatz`. A [`ScoreReduction`] gathers
8//! every note so marked into parts of their own, above the score with the
9//! marks taken out of its lyrics. A [`PartReduction`] answers when, and how
10//! loudly, each part or group of parts plays.
11//!
12//! ```
13//! use music21_rs::analysis::reduction::ScoreReduction;
14//! use music21_rs::stream::{Stream, StreamElement, StreamKind};
15//! use music21_rs::tinynotation::from_tiny_notation;
16//!
17//! let mut part = from_tiny_notation("4/4 c4 d e f g1")?;
18//! part.set_kind(StreamKind::Part);
19//! part.for_each_mut(&mut |_, element| {
20//!     if let StreamElement::Note(note) = element
21//!         && note.pitch().name() == "E"
22//!     {
23//!         note.add_lyric("::/o:5/tb:3", None, false)
24//!             .expect("a note takes a lyric");
25//!     }
26//! });
27//! let mut score = Stream::with_kind(StreamKind::Score);
28//! score.insert(0.0, part);
29//! let mut reduction = ScoreReduction::new();
30//! reduction.set_score(score);
31//! let reduced = reduction.reduce()?;
32//! let notes = reduced.parts()[0].notes();
33//! assert_eq!(notes.len(), 1);
34//! # Ok::<(), music21_rs::Error>(())
35//! ```
36
37use crate::{
38    defaults::{FloatType, IntegerType},
39    duration::Duration,
40    error::{Error, Result},
41    expressions::TextExpression,
42    instrument::Instrument,
43    makenotation::op_frac,
44    notation::{Lyric, StemDirection},
45    note::Note,
46    pitch::Pitch,
47    rest::Rest,
48    stream::{Stream, StreamElement, StreamEvent, StreamKind, TemplateOptions},
49};
50
51/// How a lyric marks its note for a reduction: music21's `ReductiveNote`
52/// parameters, each as the lyric writes it. The keys are `p` for the pitch
53/// taken from a chord, `o` the octave, `nf` the notehead fill, `sd` the stem
54/// direction, `g` the group, `v` the voice, `ta` text above and `tb` text
55/// below.
56#[derive(Clone, Debug, Default, PartialEq, Eq)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct ReductiveParameters {
59    /// The pitch name taken from a chord: `p`.
60    pub pitch: Option<String>,
61    /// The octave the note is written in: `o`.
62    pub octave: Option<String>,
63    /// Whether the notehead is filled: `nf`, `yes` or `no` or a fill
64    /// music21 names.
65    pub notehead_fill: Option<String>,
66    /// Which way the stem points: `sd`, no stem unless said.
67    pub stem_direction: Option<String>,
68    /// The group whose part the note goes in: `g`.
69    pub group: Option<String>,
70    /// The voice the note goes in: `v`.
71    pub voice: Option<String>,
72    /// Text written above the note: `ta`.
73    pub text_above: Option<String>,
74    /// Text written below the note, as a lyric: `tb`.
75    pub text_below: Option<String>,
76}
77
78impl ReductiveParameters {
79    /// The parameters a lyric says, or nothing for a lyric that does not
80    /// start `::`. Each `/`-separated `key:value` after the first sets the
81    /// parameter the key names; one naming none is passed over, as is
82    /// anything without a `:`. The stem direction is `noStem` unless said.
83    ///
84    /// # Errors
85    ///
86    /// A key written in capitals, which music21 recognizes but cannot look
87    /// up, or an argument with more than one `:`.
88    pub fn parse(spec: &str) -> Result<Option<Self>> {
89        let spec = spec.trim();
90        if !spec.starts_with("::") {
91            return Ok(None);
92        }
93        let mut parameters = Self {
94            stem_direction: Some("noStem".to_string()),
95            ..Self::default()
96        };
97        for argument in spec.split('/').skip(1) {
98            if !argument.contains(':') {
99                continue;
100            }
101            let [key, value] = argument.split(':').collect::<Vec<_>>()[..] else {
102                return Err(Error::Analysis(format!(
103                    "too many values to unpack in {argument:?}"
104                )));
105            };
106            let key = key.trim();
107            let value = Some(value.trim().to_string());
108            let slot = match key.to_lowercase().as_str() {
109                "p" => &mut parameters.pitch,
110                "o" => &mut parameters.octave,
111                "nf" => &mut parameters.notehead_fill,
112                "sd" => &mut parameters.stem_direction,
113                "g" => &mut parameters.group,
114                "v" => &mut parameters.voice,
115                "ta" => &mut parameters.text_above,
116                "tb" => &mut parameters.text_below,
117                _ => continue,
118            };
119            if key.chars().any(char::is_uppercase) {
120                return Err(Error::Analysis(format!("KeyError: {key:?}")));
121            }
122            *slot = value;
123        }
124        Ok(Some(parameters))
125    }
126}
127
128/// A note marked by a lyric for a reduction, with where it stands: music21's
129/// `ReductiveNote`.
130#[derive(Clone, Debug)]
131pub struct ReductiveNote {
132    parameters: ReductiveParameters,
133    parsed: bool,
134    note: StreamElement,
135    measure_index: usize,
136    measure_offset: FloatType,
137}
138
139impl ReductiveNote {
140    /// The note `note`, marked by the lyric `spec`, in the measure of that
141    /// index at that offset. A lyric not starting `::` marks nothing, and
142    /// leaves the parameters at their defaults.
143    ///
144    /// # Errors
145    ///
146    /// A lyric [`ReductiveParameters::parse`] refuses.
147    pub fn new(
148        spec: &str,
149        note: StreamElement,
150        measure_index: usize,
151        measure_offset: FloatType,
152    ) -> Result<Self> {
153        let parsed = ReductiveParameters::parse(spec)?;
154        Ok(Self {
155            parsed: parsed.is_some(),
156            parameters: parsed.unwrap_or_else(|| ReductiveParameters {
157                stem_direction: Some("noStem".to_string()),
158                ..ReductiveParameters::default()
159            }),
160            note,
161            measure_index,
162            measure_offset,
163        })
164    }
165
166    /// Whether the lyric marked the note at all: music21's `isParsed`.
167    pub fn is_parsed(&self) -> bool {
168        self.parsed
169    }
170
171    /// What the lyric says.
172    pub fn parameters(&self) -> &ReductiveParameters {
173        &self.parameters
174    }
175
176    /// The note marked.
177    pub fn note(&self) -> &StreamElement {
178        &self.note
179    }
180
181    /// Which measure of its part the note is in, counting from nought.
182    pub fn measure_index(&self) -> usize {
183        self.measure_index
184    }
185
186    /// Where in its measure, or its voice, the note starts.
187    pub fn measure_offset(&self) -> FloatType {
188        self.measure_offset
189    }
190
191    /// The note as the reduction writes it, and the text above it if any:
192    /// music21's `getNoteAndTextExpression`.
193    ///
194    /// A chord gives the last of its notes named as the `p` parameter says,
195    /// or its highest where none is said, the first of equally high ones.
196    /// The note keeps its pitch and length but loses
197    /// its lyrics, tie, expressions, articulations and dots; its accidental,
198    /// if written, is shown. The octave, stem direction and notehead fill
199    /// are then set as said, and the text below added as a lyric.
200    ///
201    /// # Errors
202    ///
203    /// A chord with no note of the name, or no note, an unpitched note or percussion
204    /// chord, or a parameter music21 cannot read: an octave not a number, a
205    /// stem direction or notehead fill it does not name.
206    pub fn note_and_text_expression(&self) -> Result<(Note, Option<TextExpression>)> {
207        let components: Vec<Note> = match &self.note {
208            StreamElement::Note(_) => Vec::new(),
209            StreamElement::Chord(chord) => chord
210                .notes()
211                .iter()
212                .map(|note| {
213                    let mut note = note.clone();
214                    if let Some(duration) = chord.duration() {
215                        note.set_duration(duration.clone());
216                    }
217                    note
218                })
219                .collect(),
220            StreamElement::ChordSymbol(symbol) => symbol
221                .pitches()?
222                .into_iter()
223                .map(Note::from_pitch)
224                .collect(),
225            other => {
226                return Err(Error::Analysis(format!(
227                    "{other:?} has no attribute 'pitch'"
228                )));
229            }
230        };
231        let mut note = match (&self.note, &self.parameters.pitch) {
232            (StreamElement::Note(note), _) => note.clone(),
233            (_, Some(name)) => {
234                let wanted = Pitch::from_name(name.as_str())?.name().to_lowercase();
235                components
236                    .into_iter()
237                    .rfind(|note| note.pitch().name().to_lowercase() == wanted)
238                    .ok_or_else(|| {
239                        Error::Analysis(format!(
240                            "Could not find pitch, {:?} in the note",
241                            self.parameters.pitch
242                        ))
243                    })?
244            }
245            (_, None) => components
246                .into_iter()
247                .reduce(|highest, note| {
248                    if note.pitch().ps() > highest.pitch().ps() {
249                        note
250                    } else {
251                        highest
252                    }
253                })
254                .ok_or_else(|| Error::Analysis("max() iterable argument is empty".to_string()))?,
255        };
256        note.lyrics_mut().clear();
257        note.set_tie(None);
258        note.expressions_mut().clear();
259        note.articulations_mut().clear();
260        let mut duration = note.duration().cloned().unwrap_or_default();
261        duration.set_dots(0)?;
262        note.set_duration(duration);
263        let mut pitch = note.pitch().clone();
264        if let Some(accidental) = pitch.written_accidental_mut() {
265            accidental.set_display_status(Some(true));
266        }
267        if let Some(octave) = self.parameters.octave.as_deref().filter(|o| !o.is_empty()) {
268            let octave: IntegerType = octave.parse().map_err(|_| {
269                Error::Analysis(format!(
270                    "invalid literal for int() with base 10: {octave:?}"
271                ))
272            })?;
273            pitch.set_octave(Some(octave));
274        }
275        note.set_pitch(pitch);
276        if let Some(stem) = &self.parameters.stem_direction {
277            let direction = StemDirection::from_name(stem)
278                .map_err(|_| Error::Analysis(format!("not a valid stem direction name: {stem}")))?;
279            note.set_stem_direction(direction);
280        }
281        if let Some(fill) = self
282            .parameters
283            .notehead_fill
284            .as_deref()
285            .filter(|f| !f.is_empty())
286        {
287            let fill = match fill {
288                "none" | "default" => None,
289                "yes" | "filled" => Some(true),
290                "no" | "notfilled" => Some(false),
291                other => {
292                    return Err(Error::Analysis(format!(
293                        "not a valid notehead fill value: {other:?}"
294                    )));
295                }
296            };
297            note.set_notehead_fill(fill);
298        }
299        if let Some(text) = &self.parameters.text_below {
300            note.add_lyric(text, None, false)?;
301        }
302        let text = self.parameters.text_above.clone().map(TextExpression::new);
303        Ok((note, text))
304    }
305}
306
307/// A score with its marked notes drawn out into parts of their own:
308/// music21's `ScoreReduction`.
309///
310/// Each group the marks name has a part, made from the first part's
311/// template, with the marked notes in the voices the marks name. The parts
312/// stand above the chord reduction's parts, if one is given, and the score's,
313/// whose marks are taken out of their lyrics.
314#[derive(Clone, Debug, Default)]
315pub struct ScoreReduction {
316    score: Option<Stream>,
317    chord_reduction: Option<Stream>,
318}
319
320impl ScoreReduction {
321    /// A reduction of nothing yet.
322    pub fn new() -> Self {
323        Self::default()
324    }
325
326    /// The score reduced.
327    pub fn score(&self) -> Option<&Stream> {
328        self.score.as_ref()
329    }
330
331    /// Sets the score reduced. A stream holding no parts is taken as the
332    /// one part of a score.
333    pub fn set_score(&mut self, score: Stream) {
334        self.score = Some(as_score(score));
335    }
336
337    /// A score of chords whose marked notes go in the reduction too, and
338    /// whose parts are written below it: music21's `chordReduction`.
339    pub fn chord_reduction(&self) -> Option<&Stream> {
340        self.chord_reduction.as_ref()
341    }
342
343    /// Sets the chord reduction. A stream holding no parts is taken as the
344    /// one part of a score.
345    pub fn set_chord_reduction(&mut self, chords: Stream) {
346        self.chord_reduction = Some(as_score(chords));
347    }
348
349    /// The reduction: music21's `reduce`.
350    ///
351    /// The marked notes are gathered, the chord reduction's first and then
352    /// the score's, part by part and measure by measure, each lyric of a
353    /// note once. Each group named has a part: the first part's template
354    /// without its voices, its id and an instrument's part name the group.
355    /// Where only one group is named, or none, every note goes in its part.
356    /// A measure given a note loses its rests for a voice for each voice
357    /// named, and each note is added into the voice its mark names, or the
358    /// first, as [`Stream::insert_into_note_or_chord`] adds it, with its text
359    /// above in the measure. Each voice given notes has its gaps filled with
360    /// rests; voices left empty go, and one left alone is flattened into its
361    /// measure; and every rest of the part is hidden.
362    ///
363    /// # Errors
364    ///
365    /// Nothing to reduce; a lyric with no text, or one marking a note
366    /// [`ReductiveNote`] cannot write; a mark in a measure the first part
367    /// has not got; or two notes added where music21 refuses them.
368    pub fn reduce(&self) -> Result<Stream> {
369        if self.score.is_none() && self.chord_reduction.is_none() {
370            return Err(Error::Analysis("no score defined to reduce".to_string()));
371        }
372        let mut chords = self.chord_reduction.clone();
373        let mut score = self.score.clone();
374        let mut notes: Vec<(usize, String, ReductiveNote)> = Vec::new();
375        let mut serial = 0;
376        for source in [&mut chords, &mut score].into_iter().flatten() {
377            extract(source, &mut notes, &mut serial)?;
378        }
379        let mut groups: Vec<Option<String>> = Vec::new();
380        let mut voices: Vec<Option<String>> = Vec::new();
381        for (_, _, note) in &notes {
382            for (names, name) in [
383                (&mut groups, &note.parameters.group),
384                (&mut voices, &note.parameters.voice),
385            ] {
386                if !names.contains(name) {
387                    names.push(name.clone());
388                }
389                if names.len() == 2 && names.contains(&None) {
390                    names.retain(Option::is_some);
391                }
392            }
393        }
394        let one_group = groups.len() == 1;
395        let one_voice = voices.len() == 1;
396
397        let source = match (&score, &chords) {
398            (Some(score), _) if !score.events().is_empty() => score,
399            (_, Some(chords)) => chords,
400            _ => return Err(Error::Analysis("no part to make a template of".to_string())),
401        };
402        let first = source
403            .parts()
404            .into_iter()
405            .next()
406            .ok_or_else(|| Error::Analysis("no part to make a template of".to_string()))?;
407        let template = first.template(&TemplateOptions {
408            retain_voices: false,
409            ..TemplateOptions::default()
410        });
411
412        let mut reduced = Stream::with_kind(StreamKind::Score);
413        let mut parts: Vec<Stream> = Vec::new();
414        for group in &groups {
415            let mut part = template.clone();
416            part.set_id(group.clone());
417            let mut instrument = Instrument::new();
418            instrument.set_part_name(group.clone());
419            part.insert_sorted(vec![StreamEvent::new(0.0, instrument)]);
420            for (_, _, note) in &notes {
421                if one_group || note.parameters.group == *group {
422                    place(&mut part, note, &voices, one_voice)?;
423                }
424            }
425            for event in part.events_mut() {
426                if let StreamElement::Stream(measure) = event.element_mut()
427                    && measure.kind() == StreamKind::Measure
428                {
429                    finish(measure)?;
430                }
431            }
432            parts.push(part);
433        }
434        for source in [chords, score].into_iter().flatten() {
435            parts.extend(source.parts().into_iter().cloned());
436        }
437        reduced.insert_sorted(
438            parts
439                .into_iter()
440                .map(|part| StreamEvent::new(0.0, part))
441                .collect(),
442        );
443        Ok(reduced)
444    }
445}
446
447/// A stream holding parts as it is, anything else as the one part of a
448/// score.
449fn as_score(stream: Stream) -> Stream {
450    if stream.has_part_like_streams() {
451        return stream;
452    }
453    let mut score = Stream::with_kind(StreamKind::Score);
454    score.insert(0.0, stream);
455    score
456}
457
458/// Whether music21 counts an element among a stream's `notes`.
459fn is_note(element: &StreamElement) -> bool {
460    matches!(
461        element,
462        StreamElement::Note(_)
463            | StreamElement::Chord(_)
464            | StreamElement::ChordSymbol(_)
465            | StreamElement::Unpitched(_)
466            | StreamElement::PercussionChord(_)
467    )
468}
469
470/// An element's lyrics, where it has any.
471fn lyrics_mut(element: &mut StreamElement) -> Option<&mut Vec<Lyric>> {
472    match element {
473        StreamElement::Note(note) => Some(note.lyrics_mut()),
474        StreamElement::Chord(chord) => chord.notes_mut().first_mut().map(Note::lyrics_mut),
475        StreamElement::ChordSymbol(symbol) => Some(symbol.lyrics_mut()),
476        StreamElement::Unpitched(unpitched) => Some(unpitched.written_mut().lyrics_mut()),
477        StreamElement::PercussionChord(chord) => chord
478            .written_mut()
479            .notes_mut()
480            .first_mut()
481            .map(Note::lyrics_mut),
482        _ => None,
483    }
484}
485
486/// Gathers the notes a score's lyrics mark, each lyric of a note once, and
487/// blanks the lyrics that marked them: music21's `_extractReductionEvents`.
488/// Each note is told apart by `serial`, counted across every score
489/// gathered from.
490fn extract(
491    score: &mut Stream,
492    notes: &mut Vec<(usize, String, ReductiveNote)>,
493    serial: &mut usize,
494) -> Result<()> {
495    for event in score.events_mut() {
496        let StreamElement::Stream(part) = event.element_mut() else {
497            continue;
498        };
499        if !matches!(part.kind(), StreamKind::Part | StreamKind::PartStaff) {
500            continue;
501        }
502        let mut index = 0;
503        for event in part.events_mut() {
504            let StreamElement::Stream(measure) = event.element_mut() else {
505                continue;
506            };
507            if measure.kind() != StreamKind::Measure {
508                continue;
509            }
510            extract_measure(measure, index, 0, None, notes, serial)?;
511            index += 1;
512        }
513    }
514    Ok(())
515}
516
517/// [`extract`] within a measure, `depth` streams down. A note directly in
518/// the measure, or directly in one of its voices, stands at its offset
519/// there; one deeper is read as at the start, as music21 reads it.
520fn extract_measure(
521    stream: &mut Stream,
522    index: usize,
523    depth: usize,
524    in_voice: Option<bool>,
525    notes: &mut Vec<(usize, String, ReductiveNote)>,
526    serial: &mut usize,
527) -> Result<()> {
528    for event in stream.events_mut() {
529        let offset = event.offset();
530        let element = event.element_mut();
531        if let StreamElement::Stream(inner) = element {
532            let voice = depth == 0 && inner.kind() == StreamKind::Voice;
533            extract_measure(inner, index, depth + 1, Some(voice), notes, serial)?;
534            continue;
535        }
536        if !is_note(element) {
537            continue;
538        }
539        let note_serial = *serial;
540        *serial += 1;
541        let Some(lyrics) = lyrics_mut(element).map(|lyrics| lyrics.clone()) else {
542            continue;
543        };
544        if lyrics.is_empty() {
545            continue;
546        }
547        let measure_offset = match (depth, in_voice) {
548            (0, _) | (1, Some(true)) => offset,
549            _ => 0.0,
550        };
551        let mut marked = Vec::new();
552        for (position, lyric) in lyrics.iter().enumerate() {
553            let text = lyric
554                .explicit_text()
555                .ok_or_else(|| Error::Analysis("a lyric with no text".to_string()))?;
556            let note = ReductiveNote::new(&text, element.clone(), index, measure_offset)?;
557            if note.is_parsed() {
558                match notes
559                    .iter_mut()
560                    .find(|(serial, known, _)| *serial == note_serial && *known == text)
561                {
562                    Some(entry) => entry.2 = note,
563                    None => notes.push((note_serial, text, note)),
564                }
565                marked.push(position);
566            }
567        }
568        if let Some(lyrics) = lyrics_mut(element) {
569            for position in marked {
570                lyrics[position] = Lyric::unsung();
571            }
572        }
573    }
574    Ok(())
575}
576
577/// Puts a marked note in the reduction's part: into the voice its mark
578/// names, or the measure's first, its text above in the measure.
579fn place(
580    part: &mut Stream,
581    note: &ReductiveNote,
582    voices: &[Option<String>],
583    one_voice: bool,
584) -> Result<()> {
585    let measure = part
586        .events_mut()
587        .iter_mut()
588        .filter_map(|event| match event.element_mut() {
589            StreamElement::Stream(measure) if measure.kind() == StreamKind::Measure => {
590                Some(measure)
591            }
592            _ => None,
593        })
594        .nth(note.measure_index)
595        .ok_or_else(|| Error::Analysis("list index out of range".to_string()))?;
596    if measure.voices().is_empty() {
597        measure.retain_leaves(&mut |_, element| !matches!(element, StreamElement::Rest(_)));
598        for id in voices {
599            let mut voice = Stream::with_kind(StreamKind::Voice);
600            voice.set_id(id.clone());
601            measure.insert_sorted(vec![StreamEvent::new(0.0, voice)]);
602        }
603    }
604    let wanted = note
605        .parameters
606        .voice
607        .as_deref()
608        .filter(|_| !one_voice)
609        .map(str::to_lowercase);
610    let (written, text) = note.note_and_text_expression()?;
611    let mut voices_here =
612        measure
613            .events_mut()
614            .iter_mut()
615            .filter_map(|event| match event.element_mut() {
616                StreamElement::Stream(voice) if voice.kind() == StreamKind::Voice => Some(voice),
617                _ => None,
618            });
619    let voice = match &wanted {
620        Some(wanted) => {
621            let mut chosen = None;
622            let mut first = None;
623            for voice in voices_here {
624                let named = voice.id().is_some_and(|id| id.to_lowercase() == *wanted);
625                if named {
626                    chosen = Some(voice);
627                    break;
628                }
629                if first.is_none() {
630                    first = Some(voice);
631                }
632            }
633            chosen.or(first)
634        }
635        None => voices_here.next(),
636    };
637    if let Some(voice) = voice {
638        voice.insert_into_note_or_chord(
639            note.measure_offset,
640            StreamElement::Note(written),
641            false,
642        )?;
643    }
644    if let Some(text) = text {
645        measure.insert_sorted(vec![StreamEvent::new(note.measure_offset, text)]);
646    }
647    Ok(())
648}
649
650/// A reduction's measure finished: each voice given notes has its gaps
651/// filled with rests, as music21's `makeRests` fills them; voices left empty
652/// go, and one left alone is flattened; and every rest is hidden.
653fn finish(measure: &mut Stream) -> Result<()> {
654    for event in measure.events_mut() {
655        if let StreamElement::Stream(voice) = event.element_mut()
656            && voice.kind() == StreamKind::Voice
657            && voice.events().iter().any(|event| is_note(event.element()))
658        {
659            fill_gaps(voice)?;
660        }
661    }
662    measure.flatten_unnecessary_voices(false);
663    measure.for_each_mut(&mut |_, element| {
664        if let StreamElement::Rest(rest) = element {
665            rest.set_hidden(true);
666        }
667    });
668    Ok(())
669}
670
671/// Rests from the start of a voice to where it first sounds and in every
672/// gap after: music21's `makeRests` with `fillGaps` on a voice.
673fn fill_gaps(voice: &mut Stream) -> Result<()> {
674    let mut gaps = Vec::new();
675    let mut reached: FloatType = 0.0;
676    for event in voice.events() {
677        if event.offset() > reached {
678            let length = op_frac(event.offset() - reached);
679            gaps.push(StreamEvent::new(reached, Rest::new(Duration::new(length)?)));
680        }
681        reached = op_frac(reached.max(event.offset() + event.element().quarter_length()));
682    }
683    voice.insert_sorted(gaps);
684    Ok(())
685}
686
687/// A group of a score's parts drawn as one: music21's `partGroups` entry.
688#[derive(Clone, Debug, PartialEq, Eq)]
689#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
690pub struct PartGroup {
691    /// The group's name, which is also its id in what is drawn.
692    pub name: String,
693    /// The colour the group is drawn in.
694    pub color: String,
695    /// What a part's id must hold as a whole word, ignoring case, for the
696    /// part to be in the group: the name where nothing is given. music21
697    /// reads each as a regular expression; here each is read as written, as
698    /// music21 reads one with no pattern characters.
699    pub matches: Option<Vec<String>>,
700}
701
702/// When and how loudly each part, or group of parts, of a score plays:
703/// music21's `PartReduction`, whose `getGraphHorizontalBarWeightedData`
704/// [`PartReduction::weighted_spans`] answers.
705#[derive(Clone, Debug, PartialEq, Eq)]
706#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
707pub struct PartReduction {
708    /// The groups drawn, or each part on its own where there are none.
709    pub part_groups: Option<Vec<PartGroup>>,
710    /// Whether a part plays the whole of each measure it has a note in,
711    /// rather than each run of notes: music21's `fillByMeasure`. A score
712    /// with a part holding no measures is read by its runs of notes.
713    pub fill_by_measure: bool,
714    /// Whether each stretch is cut where a dynamic changes, each piece as
715    /// loud as its dynamic: music21's `segmentByTarget`.
716    pub segment_by_target: bool,
717    /// Whether the weights are scaled so the loudest is one: music21's
718    /// `normalize`.
719    pub normalize: bool,
720    /// Whether each part's loudest is one, rather than the score's:
721    /// music21's `normalizeByPart`.
722    pub normalize_by_part: bool,
723}
724
725impl Default for PartReduction {
726    fn default() -> Self {
727        Self {
728            part_groups: None,
729            fill_by_measure: true,
730            segment_by_target: true,
731            normalize: true,
732            normalize_by_part: false,
733        }
734    }
735}
736
737/// A stretch of a part's playing, and how loud: one bar of music21's
738/// weighted horizontal bar graph.
739#[derive(Clone, Debug, PartialEq)]
740#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
741pub struct WeightedSpan {
742    /// Where the stretch starts, in quarter lengths from the score's start.
743    pub start: FloatType,
744    /// How long it lasts.
745    pub span: FloatType,
746    /// How loud it is.
747    pub weight: FloatType,
748    /// The colour it is drawn in.
749    pub color: String,
750}
751
752/// A part, or a group of parts, and the stretches it plays.
753#[derive(Clone, Debug, PartialEq)]
754#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
755pub struct PartActivity {
756    /// The group's name, or the part's id.
757    pub id: Option<String>,
758    /// The stretches it plays, in order.
759    pub spans: Vec<WeightedSpan>,
760}
761
762/// A stretch being weighed: music21's `ds` dictionaries.
763#[derive(Clone, Debug)]
764struct Stretch {
765    start: FloatType,
766    span: FloatType,
767    weight: Option<FloatType>,
768    color: String,
769}
770
771/// An element of a group's flattened parts.
772struct Flat {
773    offset: FloatType,
774    element: StreamElement,
775}
776
777/// The weight a stretch takes when nothing has said one: music21's
778/// `minValue`.
779const MIN_WEIGHT: FloatType = 0.01;
780
781impl PartReduction {
782    /// Each group's stretches of playing and their weights: music21's
783    /// `process` and `getGraphHorizontalBarWeightedData`.
784    ///
785    /// Each stretch is a measure with a note in it, from its start for its
786    /// bar's length, or a run of notes. Its weight is the mean loudness of
787    /// the dynamics starting in it; cut by dynamic, each piece runs from a
788    /// dynamic to the next, as loud as it, and the piece before the first
789    /// is as loud as the last dynamic before the stretch. A stretch with no
790    /// dynamic takes the weight of the last that had one, and the first,
791    /// with none before it, a hundredth.
792    ///
793    /// # Errors
794    ///
795    /// A stream that is not a score, a group's parts with fewer measures
796    /// than its first, or notes whose runs cannot be found.
797    pub fn weighted_spans(&self, score: &Stream) -> Result<Vec<PartActivity>> {
798        if score.kind() != StreamKind::Score {
799            return Err(Error::Analysis("provided Stream must be Score".to_string()));
800        }
801        let parts = score.parts();
802        let fill_by_measure =
803            self.fill_by_measure && parts.iter().all(|part| !part.measures().is_empty());
804        // Each group's id, colour and parts, by index.
805        let mut bundles: Vec<(Option<String>, String, Vec<usize>)> = Vec::new();
806        match &self.part_groups {
807            Some(groups) => {
808                for group in groups {
809                    let names = group
810                        .matches
811                        .clone()
812                        .unwrap_or_else(|| vec![group.name.clone()]);
813                    let held: Vec<usize> = parts
814                        .iter()
815                        .enumerate()
816                        .filter(|(_, part)| {
817                            let id = part.id().unwrap_or_default();
818                            names.iter().any(|name| holds_word(id, name))
819                        })
820                        .map(|(index, _)| index)
821                        .collect();
822                    if !held.is_empty() {
823                        bundles.push((Some(group.name.clone()), group.color.clone(), held));
824                    }
825                }
826            }
827            None => {
828                for (index, part) in parts.iter().enumerate() {
829                    bundles.push((
830                        part.id().map(str::to_string),
831                        "#666666".to_string(),
832                        vec![index],
833                    ));
834                }
835            }
836        }
837
838        let mut groups: Vec<Vec<Stretch>> = Vec::new();
839        for (_, color, held) in &bundles {
840            let flat = flatten_parts(&parts, held);
841            let mut stretches = if fill_by_measure {
842                measure_stretches(&parts, held)?
843            } else {
844                run_stretches(&flat)?
845            };
846            for stretch in &mut stretches {
847                stretch.color = color.clone();
848            }
849            if self.segment_by_target {
850                stretches = split_by_dynamics(&stretches, &flat);
851            } else {
852                for stretch in &mut stretches {
853                    let end = op_frac(stretch.start + stretch.span);
854                    let loudness: Vec<FloatType> = flat
855                        .iter()
856                        .filter(|item| {
857                            in_range(
858                                item.offset,
859                                item.element.quarter_length(),
860                                stretch.start,
861                                end,
862                                false,
863                            )
864                        })
865                        .filter_map(|item| match &item.element {
866                            StreamElement::Dynamic(dynamic) => Some(dynamic.volume_scalar()),
867                            _ => None,
868                        })
869                        .collect();
870                    stretch.weight = (!loudness.is_empty())
871                        .then(|| loudness.iter().sum::<FloatType>() / loudness.len() as FloatType);
872                }
873            }
874            extend_weights(&mut stretches);
875            groups.push(stretches);
876        }
877        if self.normalize {
878            let maxima: Vec<FloatType> = groups
879                .iter()
880                .map(|stretches| {
881                    stretches
882                        .iter()
883                        .filter_map(|stretch| stretch.weight)
884                        .fold(0.0, |max, weight| if weight > max { weight } else { max })
885                })
886                .collect();
887            let overall = maxima.iter().copied().fold(0.0, FloatType::max);
888            for (stretches, maximum) in groups.iter_mut().zip(maxima) {
889                let best = if self.normalize_by_part {
890                    maximum
891                } else {
892                    overall
893                };
894                for stretch in stretches {
895                    stretch.weight = Some(if best != 0.0 {
896                        stretch.weight.unwrap_or_default() / best
897                    } else {
898                        1.0
899                    });
900                }
901            }
902        }
903        Ok(bundles
904            .into_iter()
905            .zip(groups)
906            .map(|((id, _, _), stretches)| PartActivity {
907                id,
908                spans: stretches
909                    .into_iter()
910                    .map(|stretch| WeightedSpan {
911                        start: stretch.start,
912                        span: stretch.span,
913                        weight: stretch.weight.unwrap_or_default(),
914                        color: stretch.color,
915                    })
916                    .collect(),
917            })
918            .collect())
919    }
920}
921
922/// Whether `word` stands in `id` as a whole word, ignoring case: music21's
923/// `re.search(rf'(?<!\w)(?:{word})(?!\w)', id, re.IGNORECASE)` for a word
924/// with no pattern characters.
925fn holds_word(id: &str, word: &str) -> bool {
926    let id: Vec<char> = id.to_lowercase().chars().collect();
927    let word: Vec<char> = word.to_lowercase().chars().collect();
928    let is_word = |c: &char| c.is_alphanumeric() || *c == '_';
929    (0..=id.len().saturating_sub(word.len()))
930        .filter(|&at| id.len() >= word.len() && id[at..at + word.len()] == word[..])
931        .any(|at| {
932            !(at > 0 && is_word(&id[at - 1])) && !id.get(at + word.len()).is_some_and(is_word)
933        })
934}
935
936/// A group's parts flattened together, as music21 flattens a stream holding
937/// them: by offset, class and grace, and otherwise in the order met.
938fn flatten_parts(parts: &[&Stream], held: &[usize]) -> Vec<Flat> {
939    let mut flat: Vec<Flat> = Vec::new();
940    for &part in held {
941        for (offset, element) in parts[part].leaves() {
942            flat.push(Flat {
943                offset,
944                element: element.clone(),
945            });
946        }
947    }
948    let grace = |element: &StreamElement| element.duration().is_some_and(Duration::is_grace);
949    flat.sort_by(|left, right| {
950        left.offset
951            .partial_cmp(&right.offset)
952            .unwrap_or(std::cmp::Ordering::Equal)
953            .then_with(|| {
954                left.element
955                    .class_sort_order()
956                    .cmp(&right.element.class_sort_order())
957            })
958            .then_with(|| grace(&right.element).cmp(&grace(&left.element)))
959    });
960    flat
961}
962
963/// The meter a measure opens with: music21's `timeSignature`.
964fn meter_in(measure: &Stream) -> Option<crate::meter::TimeSignature> {
965    measure
966        .events()
967        .iter()
968        .find_map(|event| match event.element() {
969            StreamElement::TimeSignature(meter) if event.offset() == 0.0 => Some(meter.clone()),
970            _ => None,
971        })
972}
973
974/// A stretch for each measure of the group's first part in which any of
975/// its parts has a note directly, as long as the measure's bar: music21's
976/// `fillByMeasure`.
977fn measure_stretches(parts: &[&Stream], held: &[usize]) -> Result<Vec<Stretch>> {
978    let measured: Vec<Vec<(FloatType, &Stream)>> = held
979        .iter()
980        .map(|&part| {
981            parts[part]
982                .events()
983                .iter()
984                .filter_map(|event| match event.element() {
985                    StreamElement::Stream(measure) if measure.kind() == StreamKind::Measure => {
986                        Some((op_frac(event.offset()), &**measure))
987                    }
988                    _ => None,
989                })
990                .collect()
991        })
992        .collect();
993    let first = parts[held[0]];
994    let loose: Vec<(FloatType, crate::meter::TimeSignature)> = first
995        .events()
996        .iter()
997        .filter_map(|event| match event.element() {
998            StreamElement::TimeSignature(meter) => Some((event.offset(), meter.clone())),
999            _ => None,
1000        })
1001        .collect();
1002    let mut meter: Option<crate::meter::TimeSignature> = None;
1003    let mut stretches = Vec::new();
1004    for (index, (offset, measure)) in measured[0].iter().enumerate() {
1005        if let Some(own) = meter_in(measure) {
1006            meter = Some(own);
1007        }
1008        let mut active = false;
1009        for part in &measured {
1010            let (_, other) = part
1011                .get(index)
1012                .ok_or_else(|| Error::Analysis("index out of range".to_string()))?;
1013            if other.events().iter().any(|event| is_note(event.element())) {
1014                active = true;
1015                break;
1016            }
1017        }
1018        if !active {
1019            continue;
1020        }
1021        // music21's `barDuration`: the measure's meter or the one standing
1022        // before it, else the meter that fits what it holds, else what it
1023        // holds.
1024        let bar = match &meter {
1025            Some(meter) => meter.bar_quarter_length(),
1026            None => match loose.iter().rev().find(|(at, _)| at < offset) {
1027                Some((_, meter)) => meter.bar_quarter_length(),
1028                None => crate::meter::best_time_signature(measure)
1029                    .map_or_else(|_| measure.end_offset(), |meter| meter.bar_quarter_length()),
1030            },
1031        };
1032        stretches.push(Stretch {
1033            start: op_frac(*offset),
1034            span: op_frac(op_frac(*offset + bar) - *offset),
1035            weight: None,
1036            color: String::new(),
1037        });
1038    }
1039    Ok(stretches)
1040}
1041
1042/// A stretch for each run of consecutive notes of the flattened parts,
1043/// from the first note's start to the last's end: music21's stretches when
1044/// not filling by measure.
1045fn run_stretches(flat: &[Flat]) -> Result<Vec<Stretch>> {
1046    let stream = Stream::from_events(
1047        flat.iter()
1048            .map(|item| StreamEvent::new(item.offset, item.element.clone())),
1049    );
1050    let found = stream.find_consecutive_notes(&crate::stream::ConsecutiveOptions::default())?;
1051    let leaves = stream.leaves();
1052    let place = |index: usize| {
1053        let (offset, element) = leaves[index];
1054        (offset, element.quarter_length())
1055    };
1056    let mut stretches = Vec::new();
1057    let mut start: Option<FloatType> = None;
1058    let mut last: Option<usize> = None;
1059    let count = found.len();
1060    for (position, entry) in found.into_iter().enumerate() {
1061        match entry {
1062            None => {
1063                let Some(begun) = start else {
1064                    continue;
1065                };
1066                let (offset, length) = place(last.expect("a run has a last note"));
1067                let end = op_frac(offset + length);
1068                stretches.push(Stretch {
1069                    start: begun,
1070                    span: op_frac(end - begun),
1071                    weight: None,
1072                    color: String::new(),
1073                });
1074                start = None;
1075            }
1076            Some(index) if position + 1 >= count => {
1077                let (offset, length) = place(index);
1078                let begun = start.unwrap_or(offset);
1079                let end = op_frac(offset + length);
1080                stretches.push(Stretch {
1081                    start: begun,
1082                    span: op_frac(end - begun),
1083                    weight: None,
1084                    color: String::new(),
1085                });
1086                start = None;
1087            }
1088            Some(index) => {
1089                if start.is_none() {
1090                    start = Some(place(index).0);
1091                }
1092                last = Some(index);
1093            }
1094        }
1095    }
1096    Ok(stretches)
1097}
1098
1099/// Whether music21's `getElementsByOffset` finds an element at `offset`
1100/// lasting `length` between `start` and `end`, the element starting in
1101/// the span and the end itself counted only with `include_end`.
1102fn in_range(
1103    offset: FloatType,
1104    length: FloatType,
1105    start: FloatType,
1106    end: FloatType,
1107    include_end: bool,
1108) -> bool {
1109    if offset > end {
1110        return false;
1111    }
1112    if op_frac(offset + length) < start {
1113        return false;
1114    }
1115    if end <= start && length == 0.0 {
1116        return true;
1117    }
1118    if offset < start {
1119        return false;
1120    }
1121    include_end || offset != end
1122}
1123
1124/// Each stretch cut at the dynamics starting in it, each piece as loud as
1125/// its dynamic and lasting to the next, the last to the stretch's end. The
1126/// piece before the first dynamic is as loud as the last dynamic at or
1127/// before the stretch's start, and has no weight where there is none.
1128fn split_by_dynamics(stretches: &[Stretch], flat: &[Flat]) -> Vec<Stretch> {
1129    let mut cut = Vec::new();
1130    for stretch in stretches {
1131        let end = op_frac(stretch.start + stretch.span);
1132        let dynamics: Vec<(FloatType, FloatType, FloatType)> = flat
1133            .iter()
1134            .filter_map(|item| match &item.element {
1135                StreamElement::Dynamic(dynamic)
1136                    if in_range(
1137                        item.offset,
1138                        item.element.quarter_length(),
1139                        stretch.start,
1140                        end,
1141                        false,
1142                    ) =>
1143                {
1144                    Some((
1145                        item.offset,
1146                        item.element.quarter_length(),
1147                        dynamic.volume_scalar(),
1148                    ))
1149                }
1150                _ => None,
1151            })
1152            .collect();
1153        let mut first = stretch.clone();
1154        if let Some(prior) = flat.iter().rev().find_map(|item| match &item.element {
1155            StreamElement::Dynamic(dynamic) if item.offset <= stretch.start => Some(dynamic),
1156            _ => None,
1157        }) {
1158            first.weight = Some(prior.volume_scalar());
1159        }
1160        if dynamics.is_empty() {
1161            cut.push(first);
1162            continue;
1163        }
1164        // music21's `extendDuration`: each dynamic lasts to the next, the
1165        // last to where the latest of them ends.
1166        let total = dynamics
1167            .iter()
1168            .map(|(offset, length, _)| op_frac(offset + length))
1169            .fold(0.0, FloatType::max);
1170        for (index, &(offset, _, loudness)) in dynamics.iter().enumerate() {
1171            let mut span = match dynamics.get(index + 1) {
1172                Some((next, _, _)) => op_frac(next - offset),
1173                None => op_frac(total - offset),
1174            };
1175            if op_frac(offset + span) > end {
1176                span = op_frac(end - offset);
1177            }
1178            if span <= 0.001 {
1179                span = op_frac(end - offset);
1180            }
1181            let piece = Stretch {
1182                start: offset,
1183                span,
1184                weight: Some(loudness),
1185                color: stretch.color.clone(),
1186            };
1187            if index == 0 && stretch.start == offset {
1188                cut.push(Stretch {
1189                    start: stretch.start,
1190                    ..piece
1191                });
1192            } else if index == 0 {
1193                cut.push(Stretch {
1194                    span: op_frac(offset - stretch.start),
1195                    ..first.clone()
1196                });
1197                cut.push(piece);
1198            } else {
1199                cut.push(piece);
1200            }
1201        }
1202    }
1203    cut
1204}
1205
1206/// Gives each stretch with no weight the last weight before it, and the
1207/// first, or any with none before, the least: music21's `_extendSpans`.
1208fn extend_weights(stretches: &mut [Stretch]) {
1209    let mut last: Option<FloatType> = None;
1210    for (index, stretch) in stretches.iter_mut().enumerate() {
1211        if index == 0 {
1212            match stretch.weight {
1213                None => stretch.weight = Some(MIN_WEIGHT),
1214                Some(weight) => last = Some(weight),
1215            }
1216            continue;
1217        }
1218        match stretch.weight {
1219            Some(weight) if weight != 0.0 => last = Some(weight),
1220            weight => {
1221                if let Some(previous) = last.filter(|previous| *previous != 0.0) {
1222                    stretch.weight = Some(previous);
1223                } else if weight.is_none() && last.is_none() {
1224                    stretch.weight = Some(MIN_WEIGHT);
1225                }
1226            }
1227        }
1228    }
1229}
1230
1231#[cfg(test)]
1232mod tests {
1233    use super::*;
1234
1235    #[test]
1236    fn a_lyric_says_how_its_note_is_reduced() -> Result<()> {
1237        let parameters =
1238            ReductiveParameters::parse("::/o:4/nf:no/g:Ursatz/ta:3 3 200")?.expect("a mark");
1239        assert_eq!(parameters.octave.as_deref(), Some("4"));
1240        assert_eq!(parameters.notehead_fill.as_deref(), Some("no"));
1241        assert_eq!(parameters.group.as_deref(), Some("Ursatz"));
1242        assert_eq!(parameters.text_above.as_deref(), Some("3 3 200"));
1243        assert_eq!(parameters.stem_direction.as_deref(), Some("noStem"));
1244        assert_eq!(ReductiveParameters::parse("test")?, None);
1245        assert!(ReductiveParameters::parse("::/P:c").is_err());
1246        assert!(ReductiveParameters::parse("::/ta:a:b").is_err());
1247        Ok(())
1248    }
1249
1250    #[test]
1251    fn a_chord_gives_the_note_named() -> Result<()> {
1252        // music21's own test: the E of a C major chord, in the sixth octave.
1253        let chord = crate::chord::Chord::new("C4 E4 G4")?;
1254        let note = ReductiveNote::new(
1255            "::/p:e/o:6/sd:up/nf:no/ta:hi/tb:lo",
1256            StreamElement::Chord(chord.clone()),
1257            0,
1258            0.0,
1259        )?;
1260        let (written, text) = note.note_and_text_expression()?;
1261        assert_eq!(written.pitch().name_with_octave(), "E6");
1262        assert_eq!(written.stem_direction(), StemDirection::Up);
1263        assert_eq!(written.notehead_fill(), Some(false));
1264        assert_eq!(
1265            text.map(|text| text.content().to_string()),
1266            Some("hi".to_string())
1267        );
1268        // With no pitch said, a chord gives its highest note.
1269        let note = ReductiveNote::new("::", StreamElement::Chord(chord), 0, 0.0)?;
1270        assert_eq!(
1271            note.note_and_text_expression()?
1272                .0
1273                .pitch()
1274                .name_with_octave(),
1275            "G4"
1276        );
1277        Ok(())
1278    }
1279
1280    #[test]
1281    fn parts_are_weighed_by_their_dynamics() -> Result<()> {
1282        // music21's testPartReductionC: two parts of the same notes, their
1283        // dynamics in different places, weighed without normalizing.
1284        let mut score = Stream::with_kind(StreamKind::Score);
1285        let placed = [
1286            [(0.0, "p"), (2.0, "fff"), (6.0, "ppp")],
1287            [(0.0, "p"), (1.0, "fff"), (2.0, "ppp")],
1288        ];
1289        for (id, dynamics) in placed.iter().enumerate() {
1290            let mut part = Stream::with_kind(StreamKind::Part);
1291            part.set_id(Some(id.to_string()));
1292            for length in [1.0, 2.0, 1.0, 4.0] {
1293                part.push(Note::from_name("C4")?.with_duration(Duration::new(length)?));
1294            }
1295            for (offset, mark) in dynamics {
1296                part.insert(*offset, crate::dynamics::Dynamic::new(*mark));
1297            }
1298            score.insert(0.0, part);
1299        }
1300        let reduction = PartReduction {
1301            normalize: false,
1302            ..PartReduction::default()
1303        };
1304        let spans: Vec<Vec<(FloatType, FloatType, FloatType)>> = reduction
1305            .weighted_spans(&score)?
1306            .into_iter()
1307            .map(|activity| {
1308                activity
1309                    .spans
1310                    .into_iter()
1311                    .map(|span| (span.start, span.span, (span.weight * 1e9).round() / 1e9))
1312                    .collect()
1313            })
1314            .collect();
1315        assert_eq!(
1316            spans,
1317            [
1318                vec![(0.0, 2.0, 0.35), (2.0, 4.0, 0.9), (6.0, 2.0, 0.15)],
1319                vec![(0.0, 1.0, 0.35), (1.0, 1.0, 0.9), (2.0, 6.0, 0.15)],
1320            ]
1321        );
1322        Ok(())
1323    }
1324
1325    #[test]
1326    fn a_chord_reduction_alone_is_reduced_above_itself() -> Result<()> {
1327        let mut chord = crate::chord::Chord::new("G3 B3 D4")?;
1328        chord.add_lyric("::/p:g/tb:V", None, false)?;
1329        let mut measure = Stream::with_kind(StreamKind::Measure);
1330        measure.insert(0.0, chord);
1331        let mut part = Stream::with_kind(StreamKind::Part);
1332        part.insert(0.0, measure);
1333        let mut reduction = ScoreReduction::new();
1334        reduction.set_chord_reduction(part);
1335        let reduced = reduction.reduce()?;
1336        let parts = reduced.parts();
1337        assert_eq!(parts.len(), 2);
1338        let notes = parts[0].notes();
1339        assert_eq!(notes.len(), 1);
1340        let StreamElement::Note(note) = &notes[0].1 else {
1341            panic!("a note");
1342        };
1343        assert_eq!(note.pitch().name_with_octave(), "G3");
1344        assert_eq!(note.lyrics()[0].text(), "V");
1345        // The mark is taken out of the chord's lyrics.
1346        let StreamElement::Chord(chord) = &parts[1].notes()[0].1 else {
1347            panic!("a chord");
1348        };
1349        assert_eq!(chord.lyrics()[0].text(), "");
1350        Ok(())
1351    }
1352}