1use std::collections::BTreeMap;
5
6use super::{Stream, StreamElement, StreamKind, leaf_count};
7use crate::defaults::{FloatType, IntegerType};
8use crate::notation::{Lyric, TieType};
9
10#[derive(Clone, Debug, PartialEq)]
13pub enum SungLyric {
14 Lyric(Lyric),
16 Unsung,
19 Nested(Vec<SungLyric>),
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub struct SungPlace {
27 pub position: usize,
30 pub index: usize,
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
38pub enum SungAt {
39 Lyric(SungPlace),
41 Unsung,
43 Nested(Vec<SungAt>),
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub struct LyricsOptions {
51 pub ignore_barlines: bool,
56 pub recurse: bool,
59 pub skip_ties: bool,
62}
63
64impl Default for LyricsOptions {
65 fn default() -> Self {
68 Self {
69 ignore_barlines: true,
70 recurse: false,
71 skip_ties: false,
72 }
73 }
74}
75
76impl Stream {
77 pub fn lyrics(&self, options: &LyricsOptions) -> BTreeMap<IntegerType, Vec<SungLyric>> {
110 let leaves = self.leaves();
111 self.lyric_positions(options)
112 .into_iter()
113 .map(|(number, line)| (number, read_line(&line, &leaves)))
114 .collect()
115 }
116
117 pub fn lyric_positions(&self, options: &LyricsOptions) -> BTreeMap<IntegerType, Vec<SungAt>> {
125 let mut lines: BTreeMap<IntegerType, Vec<SungAt>> = BTreeMap::new();
126 let mut read = 0;
127 self.gather_lyrics(0, options, &mut lines, &mut read);
128 lines
129 }
130
131 fn gather_lyrics(
134 &self,
135 base: usize,
136 options: &LyricsOptions,
137 lines: &mut BTreeMap<IntegerType, Vec<SungAt>>,
138 read: &mut usize,
139 ) {
140 let mut place = base;
141 for event in self.events() {
142 match event.element() {
143 StreamElement::Stream(inner)
144 if options.ignore_barlines && inner.kind() == StreamKind::Measure =>
145 {
146 let mut inner_place = place;
150 for inner_event in inner.events() {
151 if is_sung_by(inner_event.element()) {
152 take(inner_place, inner_event.element(), options, lines, read);
153 }
154 inner_place += leaf_count(inner_event.element());
155 }
156 }
157 StreamElement::Stream(inner) if options.recurse => {
158 let mut nested = BTreeMap::new();
161 let mut nested_read = 0;
162 inner.gather_lyrics(place, options, &mut nested, &mut nested_read);
163 for (number, line) in nested {
164 lines.entry(number).or_default().push(SungAt::Nested(line));
165 }
166 }
167 element if is_sung_by(element) => take(place, element, options, lines, read),
168 _ => {}
171 }
172 place += leaf_count(event.element());
173 }
174 }
175}
176
177fn read_line(line: &[SungAt], leaves: &[(FloatType, &StreamElement)]) -> Vec<SungLyric> {
179 line.iter()
180 .map(|sung| match sung {
181 SungAt::Lyric(place) => match lyrics_of(leaves[place.position].1).get(place.index) {
182 Some(lyric) => SungLyric::Lyric(lyric.clone()),
183 None => SungLyric::Unsung,
184 },
185 SungAt::Unsung => SungLyric::Unsung,
186 SungAt::Nested(inner) => SungLyric::Nested(read_line(inner, leaves)),
187 })
188 .collect()
189}
190
191fn is_sung_by(element: &StreamElement) -> bool {
194 element.is_instance("NotRest")
195}
196
197fn lyrics_of(element: &StreamElement) -> &[Lyric] {
199 match element {
200 StreamElement::Note(note) => note.lyrics(),
201 StreamElement::Chord(chord) => chord.lyrics(),
202 StreamElement::ChordSymbol(symbol) => symbol.lyrics(),
203 StreamElement::Unpitched(stroke) => stroke.written().lyrics(),
204 StreamElement::PercussionChord(chord) => chord.written().lyrics(),
205 _ => &[],
206 }
207}
208
209fn continues_a_tie(element: &StreamElement) -> bool {
212 let tie = match element {
213 StreamElement::Note(note) => note.tie(),
214 StreamElement::Chord(chord) => chord.tie(),
215 StreamElement::Unpitched(stroke) => stroke.written().tie(),
216 StreamElement::PercussionChord(chord) => chord.written().tie(),
217 _ => None,
218 };
219 tie.is_some_and(|tie| tie.tie_type() != TieType::Start)
220}
221
222fn take(
225 place: usize,
226 element: &StreamElement,
227 options: &LyricsOptions,
228 lines: &mut BTreeMap<IntegerType, Vec<SungAt>>,
229 read: &mut usize,
230) {
231 if options.skip_ties && continues_a_tie(element) {
232 return;
233 }
234 let sung = lyrics_of(element);
235 if sung.is_empty() {
236 for line in lines.values_mut() {
237 line.push(SungAt::Unsung);
238 }
239 *read += 1;
240 return;
241 }
242 let mut taken: Vec<IntegerType> = Vec::new();
243 for (index, lyric) in sung.iter().enumerate() {
244 lines
246 .entry(lyric.number())
247 .or_insert_with(|| vec![SungAt::Unsung; *read])
248 .push(SungAt::Lyric(SungPlace {
249 position: place,
250 index,
251 }));
252 taken.push(lyric.number());
253 }
254 for (number, line) in &mut *lines {
255 if !taken.contains(number) {
256 line.push(SungAt::Unsung);
257 }
258 }
259 *read += 1;
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use crate::notation::{Tie, TieType};
266 use crate::note::Note;
267
268 fn sung(name: &str, words: &[(IntegerType, &str)]) -> Note {
269 let mut note = Note::from_name(name).expect("a note name");
270 for (number, text) in words {
271 let mut lyric = Lyric::new(*text);
272 lyric.set_number(*number);
273 note.lyrics_mut().push(lyric);
274 }
275 note
276 }
277
278 fn words(line: &[SungLyric]) -> Vec<String> {
279 line.iter()
280 .map(|sung| match sung {
281 SungLyric::Lyric(lyric) => lyric.text(),
282 SungLyric::Unsung => "-".to_string(),
283 SungLyric::Nested(inner) => format!("[{}]", words(inner).join(" ")),
284 })
285 .collect()
286 }
287
288 #[test]
289 fn every_note_keeps_its_place_in_each_line() {
290 let mut line = Stream::new();
291 line.push(sung("C4", &[(1, "this")]));
292 line.push(sung("D4", &[]));
293 line.push(sung("E4", &[(1, "sung")]));
294 let found = line.lyrics(&LyricsOptions::default());
295 assert_eq!(words(&found[&1]), ["this", "-", "sung"]);
296 }
297
298 #[test]
299 fn a_number_first_met_partway_starts_level_with_the_notes_before_it() {
300 let mut line = Stream::new();
301 line.push(sung("C4", &[(1, "one")]));
302 line.push(sung("D4", &[(1, "two"), (2, "second")]));
303 let found = line.lyrics(&LyricsOptions::default());
304 assert_eq!(words(&found[&1]), ["one", "two"]);
305 assert_eq!(words(&found[&2]), ["-", "second"]);
307 }
308
309 #[test]
310 fn the_notes_of_a_measure_are_read_through_it() {
311 let mut part = Stream::with_kind(StreamKind::Part);
312 for word in ["first", "second"] {
313 let mut measure = Stream::with_kind(StreamKind::Measure);
314 measure.push(sung("C4", &[(1, word)]));
315 part.push(measure);
316 }
317 let found = part.lyrics(&LyricsOptions::default());
318 assert_eq!(words(&found[&1]), ["first", "second"]);
319
320 let shallow = LyricsOptions {
323 ignore_barlines: false,
324 ..LyricsOptions::default()
325 };
326 assert!(part.lyrics(&shallow).is_empty());
327
328 let nested = LyricsOptions {
330 ignore_barlines: false,
331 recurse: true,
332 ..LyricsOptions::default()
333 };
334 assert_eq!(words(&part.lyrics(&nested)[&1]), ["[first]", "[second]"]);
335 }
336
337 #[test]
340 fn a_lines_places_name_the_notes_that_sing_it() {
341 let mut part = Stream::with_kind(StreamKind::Part);
342 let mut first = Stream::with_kind(StreamKind::Measure);
343 first.push(crate::rest::Rest::new(
346 crate::duration::Duration::new(1.0).expect("a quarter"),
347 ));
348 first.push(sung("C4", &[(1, "first"), (2, "below")]));
349 let mut second = Stream::with_kind(StreamKind::Measure);
350 second.push(sung("D4", &[(1, "second")]));
351 part.push(first);
352 part.push(second);
353
354 let lines = part.lyric_positions(&LyricsOptions::default());
355 assert_eq!(
356 lines[&1],
357 [
358 SungAt::Lyric(SungPlace {
359 position: 1,
360 index: 0
361 }),
362 SungAt::Lyric(SungPlace {
363 position: 2,
364 index: 0
365 }),
366 ]
367 );
368 assert_eq!(
371 lines[&2],
372 [
373 SungAt::Lyric(SungPlace {
374 position: 1,
375 index: 1
376 }),
377 SungAt::Unsung,
378 ]
379 );
380
381 let leaves = part.leaves();
383 for (number, word) in [(1, "first"), (2, "below")] {
384 let SungAt::Lyric(place) = lines[&number][0] else {
385 panic!("a lyric is sung there");
386 };
387 let StreamElement::Note(note) = leaves[place.position].1 else {
388 panic!("a note sings it");
389 };
390 assert_eq!(note.lyrics()[place.index].text(), word);
391 }
392 assert_eq!(
394 words(&part.lyrics(&LyricsOptions::default())[&1]),
395 ["first", "second"]
396 );
397 }
398
399 #[test]
400 fn a_note_continuing_a_tie_is_passed_over_where_asked() {
401 let mut line = Stream::new();
402 line.push(sung("C4", &[(1, "held")]));
403 let mut tied = sung("C4", &[(1, "over")]);
404 tied.set_tie(Some(Tie::new(TieType::Stop)));
405 line.push(tied);
406 assert_eq!(
407 words(&line.lyrics(&LyricsOptions::default())[&1]),
408 ["held", "over"]
409 );
410 let skipping = LyricsOptions {
411 skip_ties: true,
412 ..LyricsOptions::default()
413 };
414 assert_eq!(words(&line.lyrics(&skipping)[&1]), ["held"]);
415 }
416}