Skip to main content

music21_rs/stream/
lyrics.rs

1//! The lyrics sung in a stream, gathered by their number: music21's
2//! `Stream.lyrics`.
3
4use std::collections::BTreeMap;
5
6use super::{Stream, StreamElement, StreamKind, leaf_count};
7use crate::defaults::{FloatType, IntegerType};
8use crate::notation::{Lyric, TieType};
9
10/// What stands at one note's place in a line of lyrics: music21's
11/// `RecursiveLyricList`.
12#[derive(Clone, Debug, PartialEq)]
13pub enum SungLyric {
14    /// The lyric the note carries under this number.
15    Lyric(Lyric),
16    /// A note carrying no lyric under this number, which keeps its place
17    /// in the line so that the lyrics stay level with the notes.
18    Unsung,
19    /// What a nested stream answered, where the search recursed into it.
20    Nested(Vec<SungLyric>),
21}
22
23/// Where a lyric of a line is sung, for a caller holding something of its
24/// own beside each element: [`SungLyric`] said in positions.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub struct SungPlace {
27    /// The position among [`Stream::leaves`] of the note singing it,
28    /// counted from the stream asked.
29    pub position: usize,
30    /// Which of that note's lyrics it is.
31    pub index: usize,
32}
33
34/// [`SungLyric`] said in positions: what stands at one note's place in a
35/// line of lyrics, for a caller that keeps the objects it read the stream
36/// from.
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub enum SungAt {
39    /// The lyric sung there, by where it is.
40    Lyric(SungPlace),
41    /// A note carrying no lyric under this number, which keeps its place.
42    Unsung,
43    /// What a nested stream answered, its positions counted from the
44    /// stream asked rather than from the nested one.
45    Nested(Vec<SungAt>),
46}
47
48/// How a stream's lyrics are gathered: music21's `lyrics` keywords.
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub struct LyricsOptions {
51    /// Whether the notes of a measure are read as though the measure were
52    /// not there: `ignoreBarlines`, which music21 takes as true, so that
53    /// lyrics are gathered through the measures of a part without asking
54    /// to recurse.
55    pub ignore_barlines: bool,
56    /// Whether every nested stream is read, each answering a list of its
57    /// own inside its holder's: `recurse`.
58    pub recurse: bool,
59    /// Whether a note continuing a tie is passed over, as the syllable
60    /// was sung at the note that started it: `skipTies`.
61    pub skip_ties: bool,
62}
63
64impl Default for LyricsOptions {
65    /// music21's own defaults: measures read through, nothing else
66    /// recursed, and tied notes counted.
67    fn default() -> Self {
68        Self {
69            ignore_barlines: true,
70            recurse: false,
71            skip_ties: false,
72        }
73    }
74}
75
76impl Stream {
77    /// The lyrics sung in this stream, gathered by the number each is sung
78    /// under: music21's `lyrics`.
79    ///
80    /// Each number's line has one place for every note read, holding the
81    /// lyric sung there or [`SungLyric::Unsung`] where that note sings
82    /// nothing under that number, so the lines stay level with the notes.
83    /// A number first met partway through is padded with the notes before
84    /// it, as music21 pads it.
85    ///
86    /// ```
87    /// use music21_rs::stream::{LyricsOptions, SungLyric};
88    /// use music21_rs::notation::Lyric;
89    /// use music21_rs::{Note, Stream};
90    ///
91    /// let mut line = Stream::new();
92    /// for (name, word) in [("C4", "this"), ("D4", "is"), ("E4", "sung")] {
93    ///     let mut note = Note::from_name(name)?;
94    ///     note.lyrics_mut().push(Lyric::new(word));
95    ///     line.push(note);
96    /// }
97    ///
98    /// let sung = line.lyrics(&LyricsOptions::default());
99    /// let words: Vec<String> = sung[&1]
100    ///     .iter()
101    ///     .map(|sung| match sung {
102    ///         SungLyric::Lyric(lyric) => lyric.text(),
103    ///         _ => String::new(),
104    ///     })
105    ///     .collect();
106    /// assert_eq!(words, ["this", "is", "sung"]);
107    /// # Ok::<(), music21_rs::Error>(())
108    /// ```
109    pub fn lyrics(&self, options: &LyricsOptions) -> BTreeMap<IntegerType, Vec<SungLyric>> {
110        let leaves = self.leaves();
111        self.lyric_positions(options)
112            .into_iter()
113            .map(|(number, line)| (number, read_line(&line, &leaves)))
114            .collect()
115    }
116
117    /// [`Stream::lyrics`] in positions: each line with, for every note
118    /// read, where the lyric sung there stands.
119    ///
120    /// music21 hands back the `Lyric` objects the notes carry, so a caller
121    /// keeping an object per element -- a binding keeping what it read the
122    /// stream from -- answers with the note's own lyric by looking it up at
123    /// that position among [`Stream::leaves`].
124    pub fn lyric_positions(&self, options: &LyricsOptions) -> BTreeMap<IntegerType, Vec<SungAt>> {
125        let mut lines: BTreeMap<IntegerType, Vec<SungAt>> = BTreeMap::new();
126        let mut read = 0;
127        self.gather_lyrics(0, options, &mut lines, &mut read);
128        lines
129    }
130
131    /// The walk itself, `base` being the position of this stream's first
132    /// leaf among the leaves of the stream asked.
133    fn gather_lyrics(
134        &self,
135        base: usize,
136        options: &LyricsOptions,
137        lines: &mut BTreeMap<IntegerType, Vec<SungAt>>,
138        read: &mut usize,
139    ) {
140        let mut place = base;
141        for event in self.events() {
142            match event.element() {
143                StreamElement::Stream(inner)
144                    if options.ignore_barlines && inner.kind() == StreamKind::Measure =>
145                {
146                    // music21 reads a measure's notes as if the measure
147                    // were not there, and its own notes alone -- not those
148                    // of a voice inside it.
149                    let mut inner_place = place;
150                    for inner_event in inner.events() {
151                        if is_sung_by(inner_event.element()) {
152                            take(inner_place, inner_event.element(), options, lines, read);
153                        }
154                        inner_place += leaf_count(inner_event.element());
155                    }
156                }
157                StreamElement::Stream(inner) if options.recurse => {
158                    // The nested stream's own lines, counted from here, so
159                    // every position means the same thing.
160                    let mut nested = BTreeMap::new();
161                    let mut nested_read = 0;
162                    inner.gather_lyrics(place, options, &mut nested, &mut nested_read);
163                    for (number, line) in nested {
164                        lines.entry(number).or_default().push(SungAt::Nested(line));
165                    }
166                }
167                element if is_sung_by(element) => take(place, element, options, lines, read),
168                // Anything else says nothing about the lyrics, and a
169                // nested stream nobody asked to read is left alone.
170                _ => {}
171            }
172            place += leaf_count(event.element());
173        }
174    }
175}
176
177/// One line's lyrics, read off the leaves the positions name.
178fn read_line(line: &[SungAt], leaves: &[(FloatType, &StreamElement)]) -> Vec<SungLyric> {
179    line.iter()
180        .map(|sung| match sung {
181            SungAt::Lyric(place) => match lyrics_of(leaves[place.position].1).get(place.index) {
182                Some(lyric) => SungLyric::Lyric(lyric.clone()),
183                None => SungLyric::Unsung,
184            },
185            SungAt::Unsung => SungLyric::Unsung,
186            SungAt::Nested(inner) => SungLyric::Nested(read_line(inner, leaves)),
187        })
188        .collect()
189}
190
191/// Whether an element is one that carries lyrics: music21 reads the
192/// `NotRest`s, so a rest sings nothing and keeps no place.
193fn is_sung_by(element: &StreamElement) -> bool {
194    element.is_instance("NotRest")
195}
196
197/// The lyrics one element carries.
198fn lyrics_of(element: &StreamElement) -> &[Lyric] {
199    match element {
200        StreamElement::Note(note) => note.lyrics(),
201        StreamElement::Chord(chord) => chord.lyrics(),
202        StreamElement::ChordSymbol(symbol) => symbol.lyrics(),
203        StreamElement::Unpitched(stroke) => stroke.written().lyrics(),
204        StreamElement::PercussionChord(chord) => chord.written().lyrics(),
205        _ => &[],
206    }
207}
208
209/// Whether a note continues a tie, which `skip_ties` passes over: music21
210/// keeps the note that starts one and the note that carries none.
211fn continues_a_tie(element: &StreamElement) -> bool {
212    let tie = match element {
213        StreamElement::Note(note) => note.tie(),
214        StreamElement::Chord(chord) => chord.tie(),
215        StreamElement::Unpitched(stroke) => stroke.written().tie(),
216        StreamElement::PercussionChord(chord) => chord.written().tie(),
217        _ => None,
218    };
219    tie.is_some_and(|tie| tie.tie_type() != TieType::Start)
220}
221
222/// Takes one note's lyrics into the lines, every other line taking a place
223/// of its own so they stay level: music21's `appendLyricsFromNote`.
224fn take(
225    place: usize,
226    element: &StreamElement,
227    options: &LyricsOptions,
228    lines: &mut BTreeMap<IntegerType, Vec<SungAt>>,
229    read: &mut usize,
230) {
231    if options.skip_ties && continues_a_tie(element) {
232        return;
233    }
234    let sung = lyrics_of(element);
235    if sung.is_empty() {
236        for line in lines.values_mut() {
237            line.push(SungAt::Unsung);
238        }
239        *read += 1;
240        return;
241    }
242    let mut taken: Vec<IntegerType> = Vec::new();
243    for (index, lyric) in sung.iter().enumerate() {
244        // A number first met now starts level with the notes before it.
245        lines
246            .entry(lyric.number())
247            .or_insert_with(|| vec![SungAt::Unsung; *read])
248            .push(SungAt::Lyric(SungPlace {
249                position: place,
250                index,
251            }));
252        taken.push(lyric.number());
253    }
254    for (number, line) in &mut *lines {
255        if !taken.contains(number) {
256            line.push(SungAt::Unsung);
257        }
258    }
259    *read += 1;
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use crate::notation::{Tie, TieType};
266    use crate::note::Note;
267
268    fn sung(name: &str, words: &[(IntegerType, &str)]) -> Note {
269        let mut note = Note::from_name(name).expect("a note name");
270        for (number, text) in words {
271            let mut lyric = Lyric::new(*text);
272            lyric.set_number(*number);
273            note.lyrics_mut().push(lyric);
274        }
275        note
276    }
277
278    fn words(line: &[SungLyric]) -> Vec<String> {
279        line.iter()
280            .map(|sung| match sung {
281                SungLyric::Lyric(lyric) => lyric.text(),
282                SungLyric::Unsung => "-".to_string(),
283                SungLyric::Nested(inner) => format!("[{}]", words(inner).join(" ")),
284            })
285            .collect()
286    }
287
288    #[test]
289    fn every_note_keeps_its_place_in_each_line() {
290        let mut line = Stream::new();
291        line.push(sung("C4", &[(1, "this")]));
292        line.push(sung("D4", &[]));
293        line.push(sung("E4", &[(1, "sung")]));
294        let found = line.lyrics(&LyricsOptions::default());
295        assert_eq!(words(&found[&1]), ["this", "-", "sung"]);
296    }
297
298    #[test]
299    fn a_number_first_met_partway_starts_level_with_the_notes_before_it() {
300        let mut line = Stream::new();
301        line.push(sung("C4", &[(1, "one")]));
302        line.push(sung("D4", &[(1, "two"), (2, "second")]));
303        let found = line.lyrics(&LyricsOptions::default());
304        assert_eq!(words(&found[&1]), ["one", "two"]);
305        // The second line is padded for the note it was not sung under.
306        assert_eq!(words(&found[&2]), ["-", "second"]);
307    }
308
309    #[test]
310    fn the_notes_of_a_measure_are_read_through_it() {
311        let mut part = Stream::with_kind(StreamKind::Part);
312        for word in ["first", "second"] {
313            let mut measure = Stream::with_kind(StreamKind::Measure);
314            measure.push(sung("C4", &[(1, word)]));
315            part.push(measure);
316        }
317        let found = part.lyrics(&LyricsOptions::default());
318        assert_eq!(words(&found[&1]), ["first", "second"]);
319
320        // Asked not to read through the barlines, and not to recurse,
321        // nothing is found at all.
322        let shallow = LyricsOptions {
323            ignore_barlines: false,
324            ..LyricsOptions::default()
325        };
326        assert!(part.lyrics(&shallow).is_empty());
327
328        // Asked to recurse, each measure answers a list of its own.
329        let nested = LyricsOptions {
330            ignore_barlines: false,
331            recurse: true,
332            ..LyricsOptions::default()
333        };
334        assert_eq!(words(&part.lyrics(&nested)[&1]), ["[first]", "[second]"]);
335    }
336
337    /// The positions are how a caller holding an object per element finds
338    /// the very lyric music21 would have handed back.
339    #[test]
340    fn a_lines_places_name_the_notes_that_sing_it() {
341        let mut part = Stream::with_kind(StreamKind::Part);
342        let mut first = Stream::with_kind(StreamKind::Measure);
343        // A rest sings nothing and keeps no place in the line, but it is a
344        // leaf all the same, so the note after it is the second.
345        first.push(crate::rest::Rest::new(
346            crate::duration::Duration::new(1.0).expect("a quarter"),
347        ));
348        first.push(sung("C4", &[(1, "first"), (2, "below")]));
349        let mut second = Stream::with_kind(StreamKind::Measure);
350        second.push(sung("D4", &[(1, "second")]));
351        part.push(first);
352        part.push(second);
353
354        let lines = part.lyric_positions(&LyricsOptions::default());
355        assert_eq!(
356            lines[&1],
357            [
358                SungAt::Lyric(SungPlace {
359                    position: 1,
360                    index: 0
361                }),
362                SungAt::Lyric(SungPlace {
363                    position: 2,
364                    index: 0
365                }),
366            ]
367        );
368        // The second lyric of the first note is its second, at the same
369        // place.
370        assert_eq!(
371            lines[&2],
372            [
373                SungAt::Lyric(SungPlace {
374                    position: 1,
375                    index: 1
376                }),
377                SungAt::Unsung,
378            ]
379        );
380
381        // Each position names the note that sings it.
382        let leaves = part.leaves();
383        for (number, word) in [(1, "first"), (2, "below")] {
384            let SungAt::Lyric(place) = lines[&number][0] else {
385                panic!("a lyric is sung there");
386            };
387            let StreamElement::Note(note) = leaves[place.position].1 else {
388                panic!("a note sings it");
389            };
390            assert_eq!(note.lyrics()[place.index].text(), word);
391        }
392        // And the lyrics themselves are what the value walk answers.
393        assert_eq!(
394            words(&part.lyrics(&LyricsOptions::default())[&1]),
395            ["first", "second"]
396        );
397    }
398
399    #[test]
400    fn a_note_continuing_a_tie_is_passed_over_where_asked() {
401        let mut line = Stream::new();
402        line.push(sung("C4", &[(1, "held")]));
403        let mut tied = sung("C4", &[(1, "over")]);
404        tied.set_tie(Some(Tie::new(TieType::Stop)));
405        line.push(tied);
406        assert_eq!(
407            words(&line.lyrics(&LyricsOptions::default())[&1]),
408            ["held", "over"]
409        );
410        let skipping = LyricsOptions {
411            skip_ties: true,
412            ..LyricsOptions::default()
413        };
414        assert_eq!(words(&line.lyrics(&skipping)[&1]), ["held"]);
415    }
416}