Skip to main content

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