Skip to main content

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