1use crate::{
4 defaults::IntegerType,
5 error::{Error, Result},
6 notation::{Lyric, Syllabic},
7 stream::{Stream, StreamElement, StreamKind},
8};
9
10pub const LINE_BREAK: &str = " // ";
13
14#[derive(Clone, Debug, PartialEq, Eq, Hash)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub enum LyricIdentifier {
20 Name(String),
22 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#[derive(Clone, Debug)]
38pub struct IndexedLyric<'a> {
39 pub element: &'a StreamElement,
41 pub start: usize,
43 pub end: usize,
45 pub measure: Option<IntegerType>,
47 pub lyric: &'a Lyric,
49 pub text: String,
51 pub identifier: LyricIdentifier,
53 pub absolute_start: usize,
55 pub absolute_end: usize,
57}
58
59#[derive(Clone, Debug)]
62pub struct LyricMatch<'a> {
63 pub measure_start: Option<IntegerType>,
65 pub measure_end: Option<IntegerType>,
67 pub text: String,
69 pub indices: Vec<IndexedLyric<'a>>,
71 pub identifier: LyricIdentifier,
73}
74
75impl<'a> LyricMatch<'a> {
76 pub fn elements(&self) -> Vec<&'a StreamElement> {
78 self.indices.iter().map(|index| index.element).collect()
79 }
80}
81
82#[derive(Clone, Debug)]
100pub struct LyricSearcher<'a> {
101 index_text: String,
102 index: Vec<IndexedLyric<'a>>,
103}
104
105impl<'a> LyricSearcher<'a> {
106 pub fn new(stream: &'a Stream) -> Self {
109 Self::with_word_separator(stream, " ")
110 }
111
112 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 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 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 pub fn index_text(&self) -> &str {
199 &self.index_text
200 }
201
202 pub fn indexed(&self) -> &[IndexedLyric<'a>] {
205 &self.index
206 }
207
208 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 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
246fn 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 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}