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