Skip to main content

music21/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/// Which lyric of which note, by position: the note's place among
83/// [`Stream::leaves`] and which of its lyrics is sung there.
84///
85/// It is the by-position twin of [`IndexedLyric::element`] and
86/// [`IndexedLyric::lyric`], for a caller holding the objects the stream was
87/// read from rather than the stream's own values.
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
90pub struct LyricPlace {
91    /// Where the note stands among [`Stream::leaves`].
92    pub position: usize,
93    /// Which of that note's lyrics is sung here.
94    pub lyric: usize,
95}
96
97/// A stretch of lyrics a search found, as the text found and the run of
98/// [`LyricSearcher::indexed`] entries it touches: the by-position twin of
99/// [`LyricMatch`].
100#[derive(Clone, Debug, PartialEq, Eq)]
101pub struct LyricSpan {
102    /// The text found, which for a search of a plain string is that string.
103    pub text: String,
104    /// The entries of [`LyricSearcher::indexed`] the text is sung to, which
105    /// are always a run: the index is in the order the text reads.
106    pub indices: core::ops::Range<usize>,
107}
108
109/// One verse as it is read: its name or number, the text of it so far,
110/// each of its lyrics with the place it is sung at, and how the last one
111/// ended, which says whether the next starts a word.
112type Verse<'a> = (
113    LyricIdentifier,
114    String,
115    Vec<(IndexedLyric<'a>, LyricPlace)>,
116    Option<Syllabic>,
117);
118
119/// One note or chord a walk met that may carry lyrics: where it stands
120/// among the leaves, itself, its lyrics and the measure it is in.
121type Sung<'a> = (usize, &'a StreamElement, &'a [Lyric], Option<IntegerType>);
122
123/// Finds text in a stream's lyrics: music21's `LyricSearcher`. Each
124/// verse's lyrics are read as one text, syllables of a word run together
125/// and words apart, and the verses one after another with [`LINE_BREAK`]
126/// between them; a search finds each place the text comes and the lyrics
127/// and notes there.
128///
129/// ```
130/// use music21::search::LyricSearcher;
131/// use music21::tinynotation::from_tiny_notation;
132///
133/// let line = from_tiny_notation("4/4 c4_Et d_in f_terra a_pax")?;
134/// let searcher = LyricSearcher::new(&line);
135/// assert_eq!(searcher.index_text(), "Et in terra pax");
136/// let found = searcher.search("in ter")?;
137/// assert_eq!(found[0].elements().len(), 2);
138/// # Ok::<(), music21::Error>(())
139/// ```
140#[derive(Clone, Debug)]
141pub struct LyricSearcher<'a> {
142    index_text: String,
143    index: Vec<IndexedLyric<'a>>,
144    places: Vec<LyricPlace>,
145}
146
147impl<'a> LyricSearcher<'a> {
148    /// The lyrics of a stream, nested streams included, read with a space
149    /// between words.
150    pub fn new(stream: &'a Stream) -> Self {
151        Self::with_word_separator(stream, " ")
152    }
153
154    /// The lyrics of a stream read with this between words: music21's
155    /// `LyricSearcher(s, wordSeparator=...)`, indexed as its `index` does.
156    pub fn with_word_separator(stream: &'a Stream, word_separator: &str) -> Self {
157        let mut notes = Vec::new();
158        sung_notes(stream, None, &mut 0, &mut notes);
159
160        // Each verse's text and lyrics, verses in the order first sung,
161        // each lyric beside the place it is sung at.
162        let mut verses: Vec<Verse<'a>> = Vec::new();
163        for (position, element, lyrics, measure) in notes {
164            for (which, lyric) in lyrics.iter().enumerate() {
165                let Some(text) = lyric.explicit_text().filter(|text| !text.is_empty()) else {
166                    continue;
167                };
168                let identifier = LyricIdentifier::of(lyric);
169                let verse = match verses.iter().position(|(known, ..)| *known == identifier) {
170                    Some(verse) => verse,
171                    None => {
172                        verses.push((identifier.clone(), String::new(), Vec::new(), None));
173                        verses.len() - 1
174                    }
175                };
176                let (_, verse_text, indexed, last_syllabic) = &mut verses[verse];
177                let mut start = verse_text.chars().count();
178                if matches!(
179                    last_syllabic,
180                    None | Some(Syllabic::Begin | Syllabic::Middle)
181                ) {
182                    verse_text.push_str(&text);
183                } else {
184                    verse_text.push_str(word_separator);
185                    verse_text.push_str(&text);
186                    start += word_separator.chars().count();
187                }
188                let length = text.chars().count();
189                indexed.push((
190                    IndexedLyric {
191                        element,
192                        start,
193                        end: start + length,
194                        measure,
195                        lyric,
196                        text,
197                        identifier,
198                        absolute_start: 0,
199                        absolute_end: 0,
200                    },
201                    LyricPlace {
202                        position,
203                        lyric: which,
204                    },
205                ));
206                *last_syllabic = if lyric.is_composite() {
207                    lyric.components().last().and_then(Lyric::explicit_syllabic)
208                } else {
209                    lyric.explicit_syllabic()
210                };
211            }
212        }
213
214        let mut index = Vec::new();
215        let mut places = Vec::new();
216        let mut last_end = 0;
217        for (_, _, indexed, _) in &verses {
218            // A verse after one that ended with something starts after the
219            // line break.
220            let shift = if last_end != 0 {
221                last_end + LINE_BREAK.chars().count()
222            } else {
223                last_end
224            };
225            for (lyric, place) in indexed {
226                let mut lyric = lyric.clone();
227                lyric.absolute_start = lyric.start + shift;
228                lyric.absolute_end = lyric.end + shift;
229                last_end = lyric.absolute_end;
230                index.push(lyric);
231                places.push(*place);
232            }
233        }
234        let index_text = verses
235            .iter()
236            .map(|(_, text, ..)| text.as_str())
237            .collect::<Vec<_>>()
238            .join(LINE_BREAK);
239        Self {
240            index_text,
241            index,
242            places,
243        }
244    }
245
246    /// The text searched: each verse's lyrics, the verses joined with
247    /// [`LINE_BREAK`]: music21's `indexText`.
248    pub fn index_text(&self) -> &str {
249        &self.index_text
250    }
251
252    /// Every lyric with text and where it sits in [`Self::index_text`]:
253    /// music21's `indexTuples`.
254    pub fn indexed(&self) -> &[IndexedLyric<'a>] {
255        &self.index
256    }
257
258    /// Where each of [`Self::indexed`] is sung, in the same order: the
259    /// by-position twin of the index, for a caller holding the objects the
260    /// stream was read from.
261    pub fn places(&self) -> &[LyricPlace] {
262        &self.places
263    }
264
265    /// The entries of [`Self::indexed`] a stretch of [`Self::index_text`]
266    /// is sung to, counting characters from `start` up to but not
267    /// including `end`: music21's `_findObjsInIndexByPos`.
268    ///
269    /// # Errors
270    ///
271    /// A stretch no lyric is sung to, which music21 refuses: one lying
272    /// wholly in the space between words or verses.
273    pub fn indices_between(&self, start: usize, end: usize) -> Result<core::ops::Range<usize>> {
274        // music21 asks for every lyric reaching past the start and
275        // starting by the last character of the stretch.
276        let touched =
277            |lyric: &IndexedLyric<'a>| lyric.absolute_end > start && lyric.absolute_start < end;
278        let first = self.index.iter().position(touched);
279        let Some(first) = first else {
280            return Err(Error::Search(format!(
281                "Could not find position {start} in text"
282            )));
283        };
284        let last = self.index.iter().rposition(touched).unwrap_or(first);
285        Ok(first..last + 1)
286    }
287
288    /// Each place `text` comes in the lyrics, left to right, as the text
289    /// found and the run of [`Self::indexed`] it is sung to: the
290    /// by-position twin of [`Self::search`], which is built on it.
291    ///
292    /// # Errors
293    ///
294    /// As [`Self::indices_between`].
295    pub fn search_spans(&self, text: &str) -> Result<Vec<LyricSpan>> {
296        let mut spans = Vec::new();
297        for (byte_start, found) in self.index_text.match_indices(text) {
298            let start = self.index_text[..byte_start].chars().count();
299            let end = start + found.chars().count();
300            spans.push(LyricSpan {
301                text: found.to_string(),
302                indices: self.indices_between(start, end)?,
303            });
304        }
305        Ok(spans)
306    }
307
308    /// Each place `text` comes in the lyrics, left to right, none
309    /// overlapping the one before: music21's `search` of a plain string.
310    ///
311    /// # Errors
312    ///
313    /// A match that touches no lyric, which music21 refuses: one that lies
314    /// wholly in the space between words or verses.
315    pub fn search(&self, text: &str) -> Result<Vec<LyricMatch<'a>>> {
316        let mut matches = Vec::new();
317        for span in self.search_spans(text)? {
318            let indices: Vec<IndexedLyric<'a>> = self.index[span.indices].to_vec();
319            // `search_spans` answers no empty run, so both ends are there.
320            let (Some(first), Some(final_lyric)) = (indices.first(), indices.last()) else {
321                continue;
322            };
323            matches.push(LyricMatch {
324                measure_start: first.measure,
325                measure_end: final_lyric.measure,
326                text: span.text,
327                identifier: first.identifier.clone(),
328                indices,
329            });
330        }
331        Ok(matches)
332    }
333}
334
335/// The notes and chords of a stream that carry lyrics, nested ones
336/// included, in order, each with where it stands among [`Stream::leaves`]
337/// and the number of the measure it is in.
338///
339/// `leaves` counts every element that is not a stream, which is what
340/// [`Stream::leaves`] lists, so the number handed out here is a position in
341/// that list: the two walks read a stream in the same order.
342fn sung_notes<'a>(
343    stream: &'a Stream,
344    measure: Option<IntegerType>,
345    leaves: &mut usize,
346    out: &mut Vec<Sung<'a>>,
347) {
348    for event in stream.events() {
349        match event.element() {
350            element @ StreamElement::Note(note) => {
351                out.push((*leaves, element, note.lyrics(), measure));
352                *leaves += 1;
353            }
354            element @ StreamElement::Chord(chord) => {
355                out.push((*leaves, element, chord.lyrics(), measure));
356                *leaves += 1;
357            }
358            StreamElement::Stream(inner) => {
359                let measure = if inner.kind() == StreamKind::Measure {
360                    Some(inner.number())
361                } else {
362                    measure
363                };
364                sung_notes(inner, measure, leaves, out);
365            }
366            // Anything else is a leaf too, and takes a place in the list
367            // even where nothing is sung to it.
368            _ => *leaves += 1,
369        }
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use crate::tinynotation::from_tiny_notation;
377
378    #[test]
379    fn every_indexed_lyric_says_where_it_is_sung() -> Result<()> {
380        // A line with a rest in it, so the notes do not stand where they
381        // would if nothing else took a place, and a second verse on one
382        // note, so a place must say which lyric as well as which note.
383        let mut line = from_tiny_notation("4/4 c4_la r d_li e_lu f_le")?;
384        line.for_each_mut(&mut |_, element| {
385            if let StreamElement::Note(note) = element
386                && note
387                    .lyrics()
388                    .first()
389                    .is_some_and(|lyric| lyric.text() == "la")
390            {
391                let mut second = Lyric::from_raw_text("LA");
392                second.set_number(2);
393                note.lyrics_mut().push(second);
394            }
395        });
396
397        let searcher = LyricSearcher::new(&line);
398        let leaves = line.leaves();
399        assert_eq!(searcher.indexed().len(), searcher.places().len());
400        for (indexed, place) in searcher.indexed().iter().zip(searcher.places()) {
401            let (_, element) = leaves[place.position];
402            // The place names the very element the index holds, and the
403            // very lyric of it.
404            assert!(core::ptr::eq(element, indexed.element));
405            let StreamElement::Note(note) = element else {
406                panic!("a lyric is sung to a note here");
407            };
408            assert!(core::ptr::eq(&note.lyrics()[place.lyric], indexed.lyric));
409        }
410        // The second verse is the second lyric of the first note, which
411        // stands third among the leaves: the clef and the meter the reader
412        // gave the measure take places of their own.
413        let second: Vec<LyricPlace> = searcher
414            .indexed()
415            .iter()
416            .zip(searcher.places())
417            .filter(|(indexed, _)| indexed.identifier == LyricIdentifier::Number(2))
418            .map(|(_, place)| *place)
419            .collect();
420        assert_eq!(
421            second,
422            [LyricPlace {
423                position: 2,
424                lyric: 1
425            }]
426        );
427        Ok(())
428    }
429
430    #[test]
431    fn lyrics_are_indexed_as_music21_indexes_them() -> Result<()> {
432        // music21's LyricSearcher doctest: "Et in ter-ra pax" over three
433        // measures.
434        let mut line = from_tiny_notation("2/4 c2_Et d4_in f_ter f_ra a_pax")?;
435        for (position, syllabic) in [(2, Syllabic::Begin), (3, Syllabic::End)] {
436            let mut notes = 0;
437            line.for_each_mut(&mut |_, element| {
438                if let StreamElement::Note(note) = element {
439                    if notes == position {
440                        note.lyrics_mut()[0].set_syllabic(syllabic);
441                    }
442                    notes += 1;
443                }
444            });
445        }
446        let searcher = LyricSearcher::new(&line);
447        assert_eq!(searcher.index_text(), "Et in terra pax");
448        let spans: Vec<(usize, usize, Option<IntegerType>, &str)> = searcher
449            .indexed()
450            .iter()
451            .map(|lyric| (lyric.start, lyric.end, lyric.measure, lyric.text.as_str()))
452            .collect();
453        assert_eq!(
454            spans,
455            [
456                (0, 2, Some(1), "Et"),
457                (3, 5, Some(2), "in"),
458                (6, 9, Some(2), "ter"),
459                (9, 11, Some(3), "ra"),
460                (12, 15, Some(3), "pax"),
461            ]
462        );
463        let found = searcher.search("pax")?;
464        assert_eq!(found.len(), 1);
465        assert_eq!(
466            (found[0].measure_start, found[0].measure_end),
467            (Some(3), Some(3))
468        );
469        assert_eq!(found[0].text, "pax");
470        Ok(())
471    }
472}