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(¬e.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}