1use std::collections::HashMap;
5
6use crate::{
7 defaults::{FloatType, IntegerType},
8 error::{Error, Result},
9 notation::{Tie, TieType},
10 pitch::Pitch,
11 serial::{IndexedTransformation, ToneRow, TransformationConvention},
12 spanner::Spanner,
13 stream::{Stream, StreamElement, StreamKind},
14};
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
19pub enum Repetitions {
20 #[default]
23 SkipConsecutive,
24 RowsOnly,
27 IncludeAll,
29 IgnoreAll,
32}
33
34#[derive(Clone, Copy, Debug)]
36pub struct SegmentNote<'a> {
37 pub element: &'a StreamElement,
39 pub measure: Option<IntegerType>,
41 pub offset: FloatType,
44 pub leaf: usize,
47}
48
49#[derive(Clone, Debug)]
52pub struct ContiguousSegment<'a> {
53 pub notes: Vec<SegmentNote<'a>>,
55 pub part: Option<usize>,
58 pub active: Vec<u8>,
61 pub matched: Option<Vec<IntegerType>>,
64}
65
66fn pitches_of(element: &StreamElement) -> Vec<Pitch> {
68 match element {
69 StreamElement::ChordSymbol(symbol) => symbol.pitches().unwrap_or_default(),
70 _ => element.pitches(),
71 }
72}
73
74fn pitch_class(pitch: &Pitch) -> u8 {
77 (pitch.ps().round_ties_even() as i64).rem_euclid(12) as u8
78}
79
80impl ContiguousSegment<'_> {
81 pub fn start_measure(&self) -> Option<IntegerType> {
83 self.notes.first().and_then(|note| note.measure)
84 }
85
86 pub fn start_offset(&self) -> Option<FloatType> {
89 self.notes.first().map(|note| note.offset)
90 }
91
92 pub fn pitch_classes_from_bottom(&self) -> Vec<u8> {
95 self.notes
96 .iter()
97 .flat_map(|note| pitches_of(note.element))
98 .map(|pitch| pitch_class(&pitch))
99 .collect()
100 }
101
102 pub fn distinct_pitch_classes(&self) -> Vec<u8> {
105 let mut distinct = Vec::new();
106 for class in self.pitch_classes_from_bottom() {
107 if !distinct.contains(&class) {
108 distinct.push(class);
109 }
110 }
111 distinct
112 }
113
114 fn rows(&self) -> Option<(ToneRow, ToneRow)> {
115 let matched = self.matched.as_ref()?;
116 Some((
117 ToneRow::new(self.active.iter().map(|class| IntegerType::from(*class))),
118 ToneRow::new(matched.iter().copied()),
119 ))
120 }
121
122 pub fn zero_centered_transformations_from_matched(&self) -> Vec<IndexedTransformation> {
126 self.rows()
127 .map(|(active, matched)| matched.find_zero_centered_transformations(&active))
128 .unwrap_or_default()
129 }
130
131 pub fn original_centered_transformations_from_matched(&self) -> Vec<IndexedTransformation> {
134 self.rows()
135 .map(|(active, matched)| matched.find_original_centered_transformations(&active))
136 .unwrap_or_default()
137 }
138}
139
140fn parts_of(stream: &Stream) -> Vec<(Option<usize>, &Stream)> {
143 let parts: Vec<&Stream> = stream
144 .recurse()
145 .into_iter()
146 .filter_map(|(_, element)| match element {
147 StreamElement::Stream(inner)
148 if matches!(inner.kind(), StreamKind::Part | StreamKind::PartStaff) =>
149 {
150 Some(&**inner)
151 }
152 _ => None,
153 })
154 .collect();
155 if parts.is_empty() {
156 vec![(None, stream)]
157 } else {
158 parts
159 .into_iter()
160 .enumerate()
161 .map(|(index, part)| (Some(index), part))
162 .collect()
163 }
164}
165
166fn tie_of(element: &StreamElement) -> Option<&Tie> {
167 match element {
168 StreamElement::Note(note) => note.tie(),
169 StreamElement::Chord(chord) => chord.tie(),
170 StreamElement::Unpitched(stroke) => stroke.written().tie(),
171 StreamElement::PercussionChord(chord) => chord.written().tie(),
172 _ => None,
173 }
174}
175
176fn part_notes(part: &Stream) -> Vec<SegmentNote<'_>> {
180 fn walk<'a>(
181 stream: &'a Stream,
182 measure: Option<IntegerType>,
183 leaf: &mut usize,
184 out: &mut Vec<SegmentNote<'a>>,
185 ) {
186 for event in stream.events() {
187 let element = event.element();
188 if let StreamElement::Stream(inner) = element {
189 let measure = if inner.kind() == StreamKind::Measure {
190 Some(inner.number())
191 } else {
192 measure
193 };
194 walk(inner, measure, leaf, out);
195 continue;
196 }
197 let position = *leaf;
198 *leaf += 1;
199 if !matches!(
200 element,
201 StreamElement::Note(_)
202 | StreamElement::Chord(_)
203 | StreamElement::Unpitched(_)
204 | StreamElement::PercussionChord(_)
205 | StreamElement::ChordSymbol(_)
206 ) {
207 continue;
208 }
209 if tie_of(element).is_some_and(|tie| tie.tie_type() != TieType::Start) {
210 continue;
211 }
212 out.push(SegmentNote {
213 element,
214 measure,
215 offset: event.offset(),
216 leaf: position,
217 });
218 }
219 }
220 let mut out = Vec::new();
221 let measure = (part.kind() == StreamKind::Measure).then(|| part.number());
222 walk(part, measure, &mut 0, &mut out);
223 out
224}
225
226#[derive(Clone, Copy, Debug, Default)]
229pub struct ContiguousSegmentSearcher {
230 pub repetitions: Repetitions,
232 pub include_chords: bool,
234 pub trim_to_shortest_length_fast: bool,
237}
238
239struct Walk<'a> {
241 length: usize,
242 part: Option<usize>,
243 chords: Vec<SegmentNote<'a>>,
244 total: usize,
245 found: Vec<ContiguousSegment<'a>>,
246}
247
248impl<'a> Walk<'a> {
249 fn add(&mut self, notes: Vec<SegmentNote<'a>>) {
250 self.found.push(ContiguousSegment {
251 notes,
252 part: self.part,
253 active: Vec::new(),
254 matched: None,
255 });
256 }
257
258 fn drop_front(&mut self, count: usize) {
259 for removed in self.chords.drain(..count) {
260 self.total -= pitches_of(removed.element).len().min(self.total);
261 }
262 }
263}
264
265fn count(note: &SegmentNote<'_>) -> usize {
266 pitches_of(note.element).len()
267}
268
269fn no_pitches() -> Error {
270 Error::Search("a chord at the end of the segment holds no pitches".to_string())
271}
272
273impl ContiguousSegmentSearcher {
274 pub fn new(repetitions: Repetitions, include_chords: bool) -> Self {
276 Self {
277 repetitions,
278 include_chords,
279 trim_to_shortest_length_fast: false,
280 }
281 }
282
283 pub fn by_length<'a>(
291 &self,
292 stream: &'a Stream,
293 length: usize,
294 ) -> Result<Vec<ContiguousSegment<'a>>> {
295 let mut found = Vec::new();
296 for (part, part_stream) in parts_of(stream) {
297 let mut walk = Walk {
298 length,
299 part,
300 chords: Vec::new(),
301 total: 0,
302 found: Vec::new(),
303 };
304 for note in part_notes(part_stream) {
305 match (self.include_chords, self.repetitions) {
306 (false, Repetitions::SkipConsecutive) => {
307 if !same_as_last(&walk, ¬e) {
308 include_all_exclude(&mut walk, note);
309 }
310 }
311 (false, Repetitions::RowsOnly) => rows_only_exclude(&mut walk, note)?,
312 (false, Repetitions::IncludeAll) => include_all_exclude(&mut walk, note),
313 (false, Repetitions::IgnoreAll) => {
314 ignore_all_exclude(&mut walk, note, self.trim_to_shortest_length_fast);
315 }
316 (true, Repetitions::SkipConsecutive) => {
317 if !same_as_last(&walk, ¬e) {
318 include_all_include(&mut walk, note);
319 }
320 }
321 (true, Repetitions::RowsOnly) => rows_only_include(&mut walk, note),
322 (true, Repetitions::IncludeAll) => include_all_include(&mut walk, note),
323 (true, Repetitions::IgnoreAll) => {
324 ignore_all_include(&mut walk, note, self.trim_to_shortest_length_fast)?;
325 }
326 }
327 }
328 found.append(&mut walk.found);
329 }
330 Ok(found)
331 }
332}
333
334fn same_as_last(walk: &Walk<'_>, note: &SegmentNote<'_>) -> bool {
336 walk.chords
337 .last()
338 .is_some_and(|last| pitches_of(last.element) == pitches_of(note.element))
339}
340
341fn include_all_exclude<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>) {
343 if count(¬e) > 1 {
344 walk.chords.clear();
345 return;
346 }
347 walk.total += count(¬e);
348 walk.chords.push(note);
349 if walk.chords.len() == walk.length + 1 {
350 walk.chords.remove(0);
351 }
352 if walk.chords.len() == walk.length {
353 let notes = walk.chords.clone();
354 walk.add(notes);
355 }
356}
357
358fn include_all_include<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>) {
362 walk.total += count(¬e);
363 walk.chords.push(note);
364 let mut active_length = walk.total;
365 let mut to_delete = 0;
366 for start in 0..walk.chords.len() {
367 if start > 0 {
368 active_length -= count(&walk.chords[start - 1]);
369 }
370 let active = &walk.chords[start..];
371 let inner = active_length as isize
372 - (count(&active[0]) + count(&active[active.len() - 1])) as isize;
373 if active_length >= walk.length && inner <= walk.length as isize - 2 {
374 let notes = active.to_vec();
375 walk.add(notes);
376 } else if active_length >= walk.length {
377 to_delete += 1;
378 } else {
379 break;
380 }
381 }
382 walk.drop_front(to_delete);
383}
384
385fn ignore_all_exclude<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>, trim: bool) {
387 if count(¬e) > 1 {
388 walk.chords.clear();
389 return;
390 }
391 walk.chords.push(note);
392 let mut to_delete = 0;
393 for start in 0..walk.chords.len() {
394 let active = &walk.chords[start..];
395 let mut classes: Vec<u8> = active
396 .iter()
397 .flat_map(|note| pitches_of(note.element))
398 .map(|pitch| pitch_class(&pitch))
399 .collect();
400 classes.sort_unstable();
401 classes.dedup();
402 if classes.len() == walk.length {
403 let notes = active.to_vec();
404 walk.add(notes);
405 if trim {
406 to_delete += 1;
407 }
408 } else if classes.len() > walk.length {
409 to_delete += 1;
410 }
411 }
412 walk.drop_front(to_delete);
413}
414
415fn ignore_all_include<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>, trim: bool) -> Result<()> {
417 walk.chords.push(note);
418 let mut to_delete = 0;
419 for start in 0..walk.chords.len() {
420 let active = walk.chords[start..].to_vec();
421 walk.add(active.clone());
422 let mut superset = walk
423 .found
424 .last()
425 .map(ContiguousSegment::pitch_classes_from_bottom)
426 .unwrap_or_default();
427 superset.sort_unstable();
428 superset.dedup();
429 if superset.len() >= walk.length {
430 let first = pitches_of(active[0].element);
431 let last = pitches_of(active[active.len() - 1].element);
432 let mut check: Vec<u8> = active[1..active.len().saturating_sub(1).max(1)]
433 .iter()
434 .flat_map(|note| pitches_of(note.element))
435 .map(|pitch| pitch_class(&pitch))
436 .collect();
437 if active.len() < 2 {
438 check.clear();
439 }
440 check.push(pitch_class(first.last().ok_or_else(no_pitches)?));
441 check.push(pitch_class(last.first().ok_or_else(no_pitches)?));
442 check.sort_unstable();
443 check.dedup();
444 if check.len() > walk.length {
445 walk.found.pop();
446 to_delete += 1;
447 } else if trim {
448 to_delete += 1;
449 }
450 } else {
451 walk.found.pop();
452 break;
453 }
454 }
455 walk.drop_front(to_delete);
456 Ok(())
457}
458
459fn rows_only_exclude<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>) -> Result<()> {
462 if count(¬e) > 1 {
463 walk.chords.clear();
464 return Ok(());
465 }
466 if walk.chords.len() == walk.length && !walk.chords.is_empty() {
467 walk.chords.remove(0);
468 }
469 let pitch_of = |element: &StreamElement| match element {
470 StreamElement::Note(note) => Ok(pitch_class(note.pitch())),
471 _ => Err(Error::Search(
472 "only a note has a single pitch to read".to_string(),
473 )),
474 };
475 let class = pitch_of(note.element)?;
476 let existing = walk
477 .chords
478 .iter()
479 .map(|old| pitch_of(old.element))
480 .collect::<Result<Vec<u8>>>()?;
481 if existing.contains(&class) {
482 walk.chords = vec![note];
483 } else {
484 walk.chords.push(note);
485 }
486 if walk.chords.len() == walk.length {
487 let notes = walk.chords.clone();
488 walk.add(notes);
489 }
490 Ok(())
491}
492
493fn rows_only_include<'a>(walk: &mut Walk<'a>, note: SegmentNote<'a>) {
495 walk.total += count(¬e);
496 walk.chords.push(note);
497 let mut active_length = walk.total;
498 let mut to_delete = 0;
499 for start in 0..walk.chords.len() {
500 if start > 0 {
501 active_length -= count(&walk.chords[start - 1]);
502 }
503 let active = walk.chords[start..].to_vec();
504 let first_count = count(&active[0]);
505 let last_count = count(&active[active.len() - 1]);
506 let inner = active_length as isize - (first_count + last_count) as isize;
507 if active_length >= walk.length && inner <= walk.length as isize - 2 {
508 walk.add(active);
509 let superset = walk
510 .found
511 .last()
512 .map(ContiguousSegment::pitch_classes_from_bottom)
513 .unwrap_or_default();
514 let lower = (superset.len() as isize - walk.length as isize - last_count as isize + 1)
515 .max(0) as usize;
516 let upper =
517 (first_count as isize).min(superset.len() as isize - walk.length as isize + 1);
518 let is_row = (lower as isize..upper).any(|j| {
519 let window = &superset[j as usize..j as usize + walk.length];
520 let mut distinct = window.to_vec();
521 distinct.sort_unstable();
522 distinct.dedup();
523 distinct.len() == walk.length
524 });
525 if !is_row {
526 walk.found.pop();
527 }
528 } else if active_length >= walk.length {
529 to_delete += 1;
530 } else {
531 break;
532 }
533 }
534 walk.drop_front(to_delete);
535}
536
537#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
540pub enum Matching {
541 #[default]
544 Exact,
545 Transposed,
548 Transformed,
551 Multiset,
554 TransposedMultiset,
557 TransposedInvertedMultiset,
560}
561
562#[derive(Clone, Debug, PartialEq)]
564enum Normal {
565 Classes(Vec<u8>),
566 Intervals(String),
567 Row(ToneRow),
568}
569
570impl Normal {
571 fn len(&self) -> usize {
572 match self {
573 Self::Classes(classes) => classes.len(),
574 Self::Intervals(intervals) => intervals.chars().count(),
575 Self::Row(row) => row.len(),
576 }
577 }
578}
579
580fn multiset(classes: impl IntoIterator<Item = i64>) -> Vec<i64> {
582 let mut sorted: Vec<i64> = classes.into_iter().collect();
583 sorted.sort_unstable();
584 sorted
585}
586
587impl Matching {
588 fn normalize(self, segment: &[IntegerType]) -> Normal {
589 let row = ToneRow::new(segment.iter().copied());
590 match self {
591 Self::Transposed => Normal::Intervals(row.intervals_as_string()),
592 Self::Transformed => Normal::Row(row),
593 _ => Normal::Classes(row.pitch_classes().to_vec()),
594 }
595 }
596
597 fn multiset_duplicates(self) -> bool {
598 matches!(
599 self,
600 Self::Multiset | Self::TransposedMultiset | Self::TransposedInvertedMultiset
601 )
602 }
603
604 pub fn equal_segments(self, search: &[IntegerType], subset: &[IntegerType]) -> bool {
611 self.equal(&self.normalize(search), &self.normalize(subset))
612 }
613
614 fn equal(self, search: &Normal, subset: &Normal) -> bool {
615 let classes = |normal: &Normal| -> Vec<i64> {
616 match normal {
617 Normal::Classes(classes) => classes.iter().map(|class| i64::from(*class)).collect(),
618 _ => Vec::new(),
619 }
620 };
621 match self {
622 Self::Exact | Self::Transposed => search == subset,
623 Self::Transformed => match (search, subset) {
624 (Normal::Row(search), Normal::Row(subset)) => {
625 !subset.find_zero_centered_transformations(search).is_empty()
626 }
627 _ => false,
628 },
629 Self::Multiset => multiset(classes(search)) == multiset(classes(subset)),
630 Self::TransposedMultiset | Self::TransposedInvertedMultiset => {
631 let wanted = multiset(classes(subset));
632 let search = classes(search);
633 let transposed = (0..12)
634 .any(|i| multiset(search.iter().map(|p| (p + i).rem_euclid(12))) == wanted);
635 transposed
636 || (self == Self::TransposedInvertedMultiset
637 && (0..12).any(|i| {
638 multiset(search.iter().map(|p| (-(p + i)).rem_euclid(12))) == wanted
639 }))
640 }
641 }
642 }
643}
644
645enum Searched {
647 Normals(Vec<Normal>),
648 Multisets(Vec<Vec<i64>>),
649}
650
651impl Searched {
652 fn already(&mut self, matching: Matching, segment: &[IntegerType]) -> bool {
655 match self {
656 Self::Normals(seen) => {
657 let normal = matching.normalize(segment);
658 let found = if matching == Matching::Transformed {
659 seen.iter().any(|used| match (&normal, used) {
660 (Normal::Row(row), Normal::Row(used)) => {
661 !used.find_zero_centered_transformations(row).is_empty()
662 }
663 _ => false,
664 })
665 } else {
666 seen.contains(&normal)
667 };
668 if !found {
669 seen.push(normal);
670 }
671 found
672 }
673 Self::Multisets(seen) => {
674 let raw: Vec<i64> = segment.iter().map(|p| i64::from(*p)).collect();
675 if matching == Matching::TransposedInvertedMultiset
676 && (0..12).any(|i| {
677 seen.contains(&multiset(raw.iter().map(|p| (-(p + i)).rem_euclid(12))))
678 })
679 {
680 return true;
681 }
682 if (0..12)
683 .any(|i| seen.contains(&multiset(raw.iter().map(|p| (p + i).rem_euclid(12)))))
684 {
685 return true;
686 }
687 seen.push(multiset(raw.iter().map(|p| (p + 11).rem_euclid(12))));
689 false
690 }
691 }
692 }
693}
694
695#[derive(Clone, Copy, Debug, Default)]
699pub struct SegmentMatcher {
700 pub matching: Matching,
702 pub repetitions: Repetitions,
704 pub include_chords: bool,
706}
707
708impl SegmentMatcher {
709 pub fn new(matching: Matching, repetitions: Repetitions, include_chords: bool) -> Self {
712 Self {
713 matching,
714 repetitions,
715 include_chords,
716 }
717 }
718
719 pub fn find<'a>(
729 &self,
730 stream: &'a Stream,
731 search: &[Vec<IntegerType>],
732 ) -> Result<Vec<ContiguousSegment<'a>>> {
733 let searcher = ContiguousSegmentSearcher::new(self.repetitions, self.include_chords);
734 let mut by_length: HashMap<usize, Vec<ContiguousSegment<'a>>> = HashMap::new();
735 let mut matched: Vec<(usize, usize)> = Vec::new();
736 let mut searched = if matches!(
737 self.matching,
738 Matching::TransposedMultiset | Matching::TransposedInvertedMultiset
739 ) {
740 Searched::Multisets(Vec::new())
741 } else {
742 Searched::Normals(Vec::new())
743 };
744 for segment in search {
745 if searched.already(self.matching, segment) {
746 continue;
747 }
748 let normal = self.matching.normalize(segment);
749 let length = segment.len();
750 let candidates = match by_length.entry(length) {
751 std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(),
752 std::collections::hash_map::Entry::Vacant(entry) => {
753 entry.insert(searcher.by_length(stream, length)?)
754 }
755 };
756 for (index, candidate) in candidates.iter_mut().enumerate() {
757 let found = if self.repetitions == Repetitions::IgnoreAll {
758 self.match_ignore_all(candidate, &normal, length)
759 } else {
760 self.match_other(candidate, &normal, length)
761 };
762 if let Some(active) = found {
763 candidate.active = active;
764 candidate.matched = Some(segment.clone());
765 matched.push((length, index));
766 }
767 }
768 }
769 Ok(matched
770 .into_iter()
771 .map(|(length, index)| by_length[&length][index].clone())
772 .collect())
773 }
774
775 fn match_ignore_all(
778 &self,
779 candidate: &ContiguousSegment<'_>,
780 search: &Normal,
781 length: usize,
782 ) -> Option<Vec<u8>> {
783 let classes = candidate.distinct_pitch_classes();
784 let first: Vec<u8> = pitches_of(candidate.notes.first()?.element)
785 .iter()
786 .map(pitch_class)
787 .collect();
788 let before_last: Vec<u8> = candidate.notes[..candidate.notes.len() - 1]
789 .iter()
790 .flat_map(|note| pitches_of(note.element))
791 .map(|pitch| pitch_class(&pitch))
792 .collect();
793 let starts = classes.len() as isize - search.len() as isize + 1;
794 for start in 0..starts.max(0) as usize {
795 let subset = &classes[start..(start + length).min(classes.len())];
796 let as_ints: Vec<IntegerType> = subset
797 .iter()
798 .map(|class| IntegerType::from(*class))
799 .collect();
800 if !self
801 .matching
802 .equal(search, &self.matching.normalize(&as_ints))
803 {
804 continue;
805 }
806 if !first.contains(&subset[0]) {
807 continue;
808 }
809 if self.matching.multiset_duplicates()
810 || !before_last.contains(&subset[subset.len() - 1])
811 {
812 return Some(subset.to_vec());
813 }
814 }
815 None
816 }
817
818 fn match_other(
820 &self,
821 candidate: &ContiguousSegment<'_>,
822 search: &Normal,
823 length: usize,
824 ) -> Option<Vec<u8>> {
825 let classes = candidate.pitch_classes_from_bottom();
826 let first = count(candidate.notes.first()?);
827 let last = count(candidate.notes.last()?);
828 let lower = (classes.len() as isize - length as isize - last as isize + 1).max(0);
829 let upper = (first as isize).min(classes.len() as isize + 1 - length as isize);
830 for start in lower..upper {
831 let start = start as usize;
832 let subset = &classes[start..(start + length).min(classes.len())];
833 let as_ints: Vec<IntegerType> = subset
834 .iter()
835 .map(|class| IntegerType::from(*class))
836 .collect();
837 if self
838 .matching
839 .equal(search, &self.matching.normalize(&as_ints))
840 {
841 return Some(subset.to_vec());
842 }
843 }
844 None
845 }
846
847 pub fn label_places(
859 &self,
860 stream: &Stream,
861 named: &[(String, Vec<IntegerType>)],
862 convention: Option<TransformationConvention>,
863 ) -> Result<Vec<SegmentLabel>> {
864 let search: Vec<Vec<IntegerType>> =
865 named.iter().map(|(_, segment)| segment.clone()).collect();
866 let mut segments = self.find(stream, &search)?;
867 segments.sort_by(|a, b| {
868 (a.part, a.start_measure())
869 .cmp(&(b.part, b.start_measure()))
870 .then_with(|| {
871 a.start_offset()
872 .partial_cmp(&b.start_offset())
873 .unwrap_or(std::cmp::Ordering::Equal)
874 })
875 });
876 let mut found = Vec::new();
877 for segment in &segments {
878 let (Some(first), Some(last)) = (segment.notes.first(), segment.notes.last()) else {
879 continue;
880 };
881 let Some((name, _)) = named
882 .iter()
883 .find(|(_, wanted)| Some(wanted) == segment.matched.as_ref())
884 else {
885 found.push(SegmentLabel {
886 part: segment.part,
887 first: first.leaf,
888 last: last.leaf,
889 label: String::new(),
890 });
891 continue;
892 };
893 let transformations = match convention {
894 Some(TransformationConvention::OriginalCentered) => {
895 segment.original_centered_transformations_from_matched()
896 }
897 Some(TransformationConvention::ZeroCentered) => {
898 segment.zero_centered_transformations_from_matched()
899 }
900 None => Vec::new(),
901 };
902 let mut label = name.clone();
903 for (transformation, index) in transformations {
904 let convention = convention.unwrap_or(TransformationConvention::ZeroCentered);
905 label.push_str(&format!(" ,{}{index}", transformation.label(convention)));
906 }
907 found.push(SegmentLabel {
908 part: segment.part,
909 first: first.leaf,
910 last: last.leaf,
911 label,
912 });
913 }
914 Ok(found)
915 }
916
917 pub fn label(
930 &self,
931 stream: &Stream,
932 named: &[(String, Vec<IntegerType>)],
933 convention: Option<TransformationConvention>,
934 ) -> Result<Stream> {
935 let found = self.label_places(stream, named, convention)?;
936 let mut labelled = stream.clone();
937 for SegmentLabel {
938 part,
939 first,
940 last,
941 label,
942 } in found
943 {
944 let target = match part {
945 Some(part) => part_mut(&mut labelled, part)
946 .ok_or_else(|| Error::Search(format!("the stream has no part {part}")))?,
947 None => &mut labelled,
948 };
949 let spanned = if first == last {
950 vec![first]
951 } else {
952 vec![first, last]
953 };
954 target.add_spanner(Spanner::line(spanned));
955 if label.is_empty() {
956 continue;
957 }
958 let mut leaf = 0;
959 let mut result = Ok(());
960 target.for_each_mut(&mut |_, element| {
961 if leaf == first {
962 let lyrics = match element {
963 StreamElement::Note(note) => Some(note.lyrics().to_vec()),
964 StreamElement::Chord(chord) => Some(chord.lyrics().to_vec()),
965 StreamElement::ChordSymbol(symbol) => Some(symbol.lyrics().to_vec()),
966 _ => None,
967 };
968 let present = lyrics
969 .is_some_and(|lyrics| lyrics.iter().any(|lyric| lyric.text() == label));
970 if !present {
971 result = match element {
972 StreamElement::Note(note) => note.add_lyric(&label, None, false),
973 StreamElement::Chord(chord) => chord.add_lyric(&label, None, false),
974 StreamElement::ChordSymbol(symbol) => {
975 symbol.add_lyric(&label, None, false);
976 Ok(())
977 }
978 _ => Ok(()),
979 };
980 }
981 }
982 leaf += 1;
983 });
984 result?;
985 }
986 Ok(labelled)
987 }
988}
989
990#[derive(Clone, Debug, PartialEq, Eq)]
995pub struct SegmentLabel {
996 pub part: Option<usize>,
999 pub first: usize,
1001 pub last: usize,
1003 pub label: String,
1005}
1006
1007fn part_mut(stream: &mut Stream, number: usize) -> Option<&mut Stream> {
1009 fn walk<'a>(stream: &'a mut Stream, number: usize, seen: &mut usize) -> Option<&'a mut Stream> {
1010 for event in stream.events_mut() {
1011 if let StreamElement::Stream(inner) = event.element_mut() {
1012 if matches!(inner.kind(), StreamKind::Part | StreamKind::PartStaff) {
1013 if *seen == number {
1014 return Some(inner);
1015 }
1016 *seen += 1;
1017 }
1018 if let Some(found) = walk(inner, number, seen) {
1019 return Some(found);
1020 }
1021 }
1022 }
1023 None
1024 }
1025 walk(stream, number, &mut 0)
1026}
1027
1028#[cfg(test)]
1029mod tests {
1030 use super::*;
1031 use crate::tinynotation::from_tiny_notation;
1032
1033 #[test]
1034 fn segments_are_found_by_length() -> Result<()> {
1035 let line = from_tiny_notation("4/4 c4 d d e f g a b")?;
1037 let skip = ContiguousSegmentSearcher::new(Repetitions::SkipConsecutive, true);
1038 let found = skip.by_length(&line, 3)?;
1039 assert_eq!(found.len(), 5);
1040 assert_eq!(found[0].pitch_classes_from_bottom(), [0, 2, 4]);
1041 let all = ContiguousSegmentSearcher::new(Repetitions::IncludeAll, true);
1042 let found = all.by_length(&line, 3)?;
1043 assert_eq!(found.len(), 6);
1044 assert_eq!(found[1].pitch_classes_from_bottom(), [2, 2, 4]);
1045 Ok(())
1046 }
1047
1048 #[test]
1049 fn rows_are_matched_and_labelled() -> Result<()> {
1050 let line = from_tiny_notation("4/4 c4 d e f g a b c'")?;
1053 let matcher = SegmentMatcher::new(Matching::Transposed, Repetitions::SkipConsecutive, true);
1054 let found = matcher.find(&line, &[vec![0, 2, 4]])?;
1055 let actives: Vec<&[u8]> = found
1056 .iter()
1057 .map(|segment| segment.active.as_slice())
1058 .collect();
1059 assert_eq!(actives, [&[0, 2, 4][..], &[5, 7, 9], &[7, 9, 11]]);
1060 let labelled = matcher.label(&line, &[("A".to_string(), vec![0, 2, 4])], None)?;
1061 let spanners: usize = labelled.spanners().len()
1062 + labelled
1063 .parts()
1064 .iter()
1065 .map(|part| part.spanners().len())
1066 .sum::<usize>();
1067 assert_eq!(spanners, 3);
1068 let lyrics: Vec<Option<String>> = labelled
1069 .leaves()
1070 .into_iter()
1071 .filter_map(|(_, element)| match element {
1072 StreamElement::Note(note) => Some(note.lyrics().first().map(|lyric| lyric.text())),
1073 _ => None,
1074 })
1075 .collect();
1076 let a = Some("A".to_string());
1077 assert_eq!(
1078 lyrics,
1079 [a.clone(), None, None, a.clone(), a, None, None, None]
1080 );
1081 Ok(())
1082 }
1083}