Skip to main content

music21_rs/search/
lyrics.rs

1//! Lyrics searched as text: music21's `search.lyrics`.
2
3use crate::{
4    defaults::IntegerType,
5    error::{Error, Result},
6    notation::{Lyric, Syllabic},
7    stream::{Stream, StreamElement, StreamKind},
8};
9
10/// What music21 writes between the lyrics of one verse and the next in the
11/// text it searches: `LINEBREAK_TOKEN`.
12pub const LINE_BREAK: &str = " // ";
13
14/// Which verse a lyric belongs to: the name a score gave it or, failing
15/// one, its number, which music21 keeps apart even where the name is a
16/// number written out.
17#[derive(Clone, Debug, PartialEq, Eq, Hash)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub enum LyricIdentifier {
20    /// The verse's own name.
21    Name(String),
22    /// The verse's number, where it has no name.
23    Number(IntegerType),
24}
25
26impl LyricIdentifier {
27    fn of(lyric: &Lyric) -> Self {
28        match lyric.explicit_identifier() {
29            Some(name) => Self::Name(name.to_string()),
30            None => Self::Number(lyric.number()),
31        }
32    }
33}
34
35/// One lyric of a stream and where its text sits in the text searched:
36/// music21's `IndexedLyric`. Positions count characters, as Python's do.
37#[derive(Clone, Debug)]
38pub struct IndexedLyric<'a> {
39    /// The note or chord the lyric is sung to.
40    pub element: &'a StreamElement,
41    /// Where the lyric starts in its verse's text.
42    pub start: usize,
43    /// Where it ends in its verse's text.
44    pub end: usize,
45    /// The number of the measure the note is in, if it is in one.
46    pub measure: Option<IntegerType>,
47    /// The lyric itself.
48    pub lyric: &'a Lyric,
49    /// Its text.
50    pub text: String,
51    /// Its verse.
52    pub identifier: LyricIdentifier,
53    /// Where it starts in the text of every verse.
54    pub absolute_start: usize,
55    /// Where it ends in the text of every verse.
56    pub absolute_end: usize,
57}
58
59/// A stretch of lyrics a search found: music21's `search.lyrics`
60/// `SearchMatch`.
61#[derive(Clone, Debug)]
62pub struct LyricMatch<'a> {
63    /// The measure of the first lyric matched, if it is in one.
64    pub measure_start: Option<IntegerType>,
65    /// The measure of the last.
66    pub measure_end: Option<IntegerType>,
67    /// The text matched.
68    pub text: String,
69    /// Every lyric the match touches.
70    pub indices: Vec<IndexedLyric<'a>>,
71    /// The verse of the first.
72    pub identifier: LyricIdentifier,
73}
74
75impl<'a> LyricMatch<'a> {
76    /// The notes and chords the matched lyrics are sung to.
77    pub fn elements(&self) -> Vec<&'a StreamElement> {
78        self.indices.iter().map(|index| index.element).collect()
79    }
80}
81
82/// Finds text in a stream's lyrics: music21's `LyricSearcher`. Each
83/// verse's lyrics are read as one text, syllables of a word run together
84/// and words apart, and the verses one after another with [`LINE_BREAK`]
85/// between them; a search finds each place the text comes and the lyrics
86/// and notes there.
87///
88/// ```
89/// use music21_rs::search::LyricSearcher;
90/// use music21_rs::tinynotation::from_tiny_notation;
91///
92/// let line = from_tiny_notation("4/4 c4_Et d_in f_terra a_pax")?;
93/// let searcher = LyricSearcher::new(&line);
94/// assert_eq!(searcher.index_text(), "Et in terra pax");
95/// let found = searcher.search("in ter")?;
96/// assert_eq!(found[0].elements().len(), 2);
97/// # Ok::<(), music21_rs::Error>(())
98/// ```
99#[derive(Clone, Debug)]
100pub struct LyricSearcher<'a> {
101    index_text: String,
102    index: Vec<IndexedLyric<'a>>,
103}
104
105impl<'a> LyricSearcher<'a> {
106    /// The lyrics of a stream, nested streams included, read with a space
107    /// between words.
108    pub fn new(stream: &'a Stream) -> Self {
109        Self::with_word_separator(stream, " ")
110    }
111
112    /// The lyrics of a stream read with this between words: music21's
113    /// `LyricSearcher(s, wordSeparator=...)`, indexed as its `index` does.
114    pub fn with_word_separator(stream: &'a Stream, word_separator: &str) -> Self {
115        let mut notes = Vec::new();
116        sung_notes(stream, None, &mut notes);
117
118        // Each verse's text and lyrics, verses in the order first sung.
119        let mut verses: Vec<(
120            LyricIdentifier,
121            String,
122            Vec<IndexedLyric<'a>>,
123            Option<Syllabic>,
124        )> = Vec::new();
125        for (element, lyrics, measure) in notes {
126            for lyric in lyrics {
127                let Some(text) = lyric.explicit_text().filter(|text| !text.is_empty()) else {
128                    continue;
129                };
130                let identifier = LyricIdentifier::of(lyric);
131                let verse = match verses.iter().position(|(known, ..)| *known == identifier) {
132                    Some(verse) => verse,
133                    None => {
134                        verses.push((identifier.clone(), String::new(), Vec::new(), None));
135                        verses.len() - 1
136                    }
137                };
138                let (_, verse_text, indexed, last_syllabic) = &mut verses[verse];
139                let mut start = verse_text.chars().count();
140                if matches!(
141                    last_syllabic,
142                    None | Some(Syllabic::Begin | Syllabic::Middle)
143                ) {
144                    verse_text.push_str(&text);
145                } else {
146                    verse_text.push_str(word_separator);
147                    verse_text.push_str(&text);
148                    start += word_separator.chars().count();
149                }
150                let length = text.chars().count();
151                indexed.push(IndexedLyric {
152                    element,
153                    start,
154                    end: start + length,
155                    measure,
156                    lyric,
157                    text,
158                    identifier,
159                    absolute_start: 0,
160                    absolute_end: 0,
161                });
162                *last_syllabic = if lyric.is_composite() {
163                    lyric.components().last().and_then(Lyric::explicit_syllabic)
164                } else {
165                    lyric.explicit_syllabic()
166                };
167            }
168        }
169
170        let mut index = Vec::new();
171        let mut last_end = 0;
172        for (_, _, indexed, _) in &verses {
173            // A verse after one that ended with something starts after the
174            // line break.
175            let shift = if last_end != 0 {
176                last_end + LINE_BREAK.chars().count()
177            } else {
178                last_end
179            };
180            for lyric in indexed {
181                let mut lyric = lyric.clone();
182                lyric.absolute_start = lyric.start + shift;
183                lyric.absolute_end = lyric.end + shift;
184                last_end = lyric.absolute_end;
185                index.push(lyric);
186            }
187        }
188        let index_text = verses
189            .iter()
190            .map(|(_, text, ..)| text.as_str())
191            .collect::<Vec<_>>()
192            .join(LINE_BREAK);
193        Self { index_text, index }
194    }
195
196    /// The text searched: each verse's lyrics, the verses joined with
197    /// [`LINE_BREAK`]: music21's `indexText`.
198    pub fn index_text(&self) -> &str {
199        &self.index_text
200    }
201
202    /// Every lyric with text and where it sits in [`Self::index_text`]:
203    /// music21's `indexTuples`.
204    pub fn indexed(&self) -> &[IndexedLyric<'a>] {
205        &self.index
206    }
207
208    /// Each place `text` comes in the lyrics, left to right, none
209    /// overlapping the one before: music21's `search` of a plain string.
210    ///
211    /// # Errors
212    ///
213    /// A match that touches no lyric, which music21 refuses: one that lies
214    /// wholly in the space between words or verses.
215    pub fn search(&self, text: &str) -> Result<Vec<LyricMatch<'a>>> {
216        let mut matches = Vec::new();
217        for (byte_start, found) in self.index_text.match_indices(text) {
218            let start = self.index_text[..byte_start].chars().count();
219            let end = start + found.chars().count();
220            // music21 asks for every lyric reaching past the start and
221            // starting by the last character found.
222            let last = end as isize - 1;
223            let indices: Vec<IndexedLyric<'a>> = self
224                .index
225                .iter()
226                .filter(|lyric| lyric.absolute_end > start && lyric.absolute_start as isize <= last)
227                .cloned()
228                .collect();
229            let (Some(first), Some(final_lyric)) = (indices.first(), indices.last()) else {
230                return Err(Error::Search(format!(
231                    "Could not find position {start} in text"
232                )));
233            };
234            matches.push(LyricMatch {
235                measure_start: first.measure,
236                measure_end: final_lyric.measure,
237                text: found.to_string(),
238                identifier: first.identifier.clone(),
239                indices,
240            });
241        }
242        Ok(matches)
243    }
244}
245
246/// The notes and chords of a stream that carry lyrics, nested ones
247/// included, in order, each with the number of the measure it is in.
248fn sung_notes<'a>(
249    stream: &'a Stream,
250    measure: Option<IntegerType>,
251    out: &mut Vec<(&'a StreamElement, &'a [Lyric], Option<IntegerType>)>,
252) {
253    for event in stream.events() {
254        match event.element() {
255            element @ StreamElement::Note(note) => {
256                out.push((element, note.lyrics(), measure));
257            }
258            element @ StreamElement::Chord(chord) => {
259                out.push((element, chord.lyrics(), measure));
260            }
261            StreamElement::Stream(inner) => {
262                let measure = if inner.kind() == StreamKind::Measure {
263                    Some(inner.number())
264                } else {
265                    measure
266                };
267                sung_notes(inner, measure, out);
268            }
269            _ => {}
270        }
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use crate::tinynotation::from_tiny_notation;
278
279    #[test]
280    fn lyrics_are_indexed_as_music21_indexes_them() -> Result<()> {
281        // music21's LyricSearcher doctest: "Et in ter-ra pax" over three
282        // measures.
283        let mut line = from_tiny_notation("2/4 c2_Et d4_in f_ter f_ra a_pax")?;
284        for (position, syllabic) in [(2, Syllabic::Begin), (3, Syllabic::End)] {
285            let mut notes = 0;
286            line.for_each_mut(&mut |_, element| {
287                if let StreamElement::Note(note) = element {
288                    if notes == position {
289                        note.lyrics_mut()[0].set_syllabic(syllabic);
290                    }
291                    notes += 1;
292                }
293            });
294        }
295        let searcher = LyricSearcher::new(&line);
296        assert_eq!(searcher.index_text(), "Et in terra pax");
297        let spans: Vec<(usize, usize, Option<IntegerType>, &str)> = searcher
298            .indexed()
299            .iter()
300            .map(|lyric| (lyric.start, lyric.end, lyric.measure, lyric.text.as_str()))
301            .collect();
302        assert_eq!(
303            spans,
304            [
305                (0, 2, Some(1), "Et"),
306                (3, 5, Some(2), "in"),
307                (6, 9, Some(2), "ter"),
308                (9, 11, Some(3), "ra"),
309                (12, 15, Some(3), "pax"),
310            ]
311        );
312        let found = searcher.search("pax")?;
313        assert_eq!(found.len(), 1);
314        assert_eq!(
315            (found[0].measure_start, found[0].measure_end),
316            (Some(3), Some(3))
317        );
318        assert_eq!(found[0].text, "pax");
319        Ok(())
320    }
321}