1use crate::{
5 defaults::{FloatType, IntegerType},
6 error::{Error, Result},
7 makenotation::op_frac,
8 notation::{Tie, TieType},
9 pitch::Pitch,
10 stream::{Stream, StreamElement, StreamKind},
11};
12
13use super::difflib;
14
15#[derive(Clone, Copy, Debug)]
18pub struct Searched<'a> {
19 pub element: &'a StreamElement,
21 pub offset: FloatType,
23 pub measure: Option<IntegerType>,
26}
27
28pub fn recursed(stream: &Stream) -> Vec<Searched<'_>> {
31 let mut out = Vec::new();
32 let measure = (stream.kind() == StreamKind::Measure).then(|| stream.number());
33 walk(stream, 0.0, measure, true, &mut out);
34 out
35}
36
37pub fn iterated(stream: &Stream) -> Vec<Searched<'_>> {
40 let mut out = Vec::new();
41 let measure = (stream.kind() == StreamKind::Measure).then(|| stream.number());
42 walk(stream, 0.0, measure, false, &mut out);
43 out
44}
45
46pub fn notes_and_rests(stream: &Stream) -> Vec<Searched<'_>> {
49 recursed(stream)
50 .into_iter()
51 .filter(|found| is_general_note(found.element))
52 .collect()
53}
54
55fn walk<'a>(
56 stream: &'a Stream,
57 base: FloatType,
58 measure: Option<IntegerType>,
59 deep: bool,
60 out: &mut Vec<Searched<'a>>,
61) {
62 for event in stream.events() {
63 let offset = op_frac(base + event.offset());
64 let element = event.element();
65 out.push(Searched {
66 element,
67 offset,
68 measure,
69 });
70 if deep && let StreamElement::Stream(inner) = element {
71 let inner_measure = if inner.kind() == StreamKind::Measure {
72 Some(inner.number())
73 } else {
74 measure
75 };
76 walk(inner, offset, inner_measure, true, out);
77 }
78 }
79}
80
81fn is_general_note(element: &StreamElement) -> bool {
84 matches!(
85 element,
86 StreamElement::Note(_)
87 | StreamElement::Chord(_)
88 | StreamElement::Rest(_)
89 | StreamElement::Unpitched(_)
90 | StreamElement::PercussionChord(_)
91 | StreamElement::ChordSymbol(_)
92 )
93}
94
95#[derive(Clone, Debug)]
97pub enum SearchTerm {
98 Wildcard,
100 Element(StreamElement),
102 AnyLength(StreamElement),
105}
106
107impl SearchTerm {
108 fn element(&self) -> Option<&StreamElement> {
109 match self {
110 Self::Wildcard => None,
111 Self::Element(element) | Self::AnyLength(element) => Some(element),
112 }
113 }
114
115 fn any_length(&self) -> bool {
118 matches!(self, Self::Wildcard | Self::AnyLength(_))
119 }
120}
121
122impl From<StreamElement> for SearchTerm {
123 fn from(element: StreamElement) -> Self {
124 Self::Element(element)
125 }
126}
127
128fn name(element: &StreamElement) -> Option<String> {
131 match element {
132 StreamElement::Note(note) => Some(note.pitch().name()),
133 StreamElement::Rest(_) => Some("rest".to_string()),
134 StreamElement::Clef(clef) => Some(clef.name()),
135 StreamElement::Key(key) => Some(format!("{} {}", key.tonic().name(), key.mode())),
136 StreamElement::TextExpression(_) => Some("text expression".to_string()),
137 _ => None,
138 }
139}
140
141#[derive(Clone, Copy, Debug)]
145pub enum Algorithm {
146 Wildcard,
148 Rhythm,
151 NoteName,
154 Custom(fn(&StreamElement, &SearchTerm) -> Option<bool>),
156}
157
158impl Algorithm {
159 fn compare(self, element: &StreamElement, term: &SearchTerm) -> Option<bool> {
160 match self {
161 Self::Wildcard => matches!(term, SearchTerm::Wildcard).then_some(true),
162 Self::Rhythm => {
163 if term.any_length() {
164 return Some(true);
165 }
166 let wanted = term.element()?.quarter_length();
167 (wanted != element.quarter_length()).then_some(false)
168 }
169 Self::NoteName => {
170 let wanted = term.element().and_then(name);
171 match (wanted, name(element)) {
172 (Some(wanted), Some(found)) if wanted == found => None,
173 _ => Some(false),
174 }
175 }
176 Self::Custom(compare) => compare(element, term),
177 }
178 }
179}
180
181#[derive(Clone, Copy, Debug, PartialEq, Eq)]
183pub enum Filter {
184 Notes,
186 NotesAndRests,
188}
189
190#[derive(Clone, Debug)]
193pub struct SearchMatch<'a> {
194 pub index: usize,
196 pub elements: Vec<Searched<'a>>,
198}
199
200#[derive(Clone, Debug)]
229pub struct StreamSearcher {
230 pub search: Vec<SearchTerm>,
232 pub recurse: bool,
234 pub filter: Option<Filter>,
236 pub algorithms: Vec<Algorithm>,
238}
239
240impl StreamSearcher {
241 pub fn new(search: Vec<SearchTerm>) -> Self {
244 Self {
245 search,
246 recurse: false,
247 filter: None,
248 algorithms: vec![Algorithm::Wildcard],
249 }
250 }
251
252 pub fn run<'a>(&self, stream: &'a Stream) -> Result<Vec<SearchMatch<'a>>> {
258 let elements = if self.recurse {
259 recursed(stream)
260 } else {
261 iterated(stream)
262 };
263 let elements: Vec<Searched<'a>> = elements
264 .into_iter()
265 .filter(|found| match self.filter {
266 None => true,
267 Some(Filter::Notes) => matches!(
268 found.element,
269 StreamElement::Note(_)
270 | StreamElement::Chord(_)
271 | StreamElement::ChordSymbol(_)
272 ),
273 Some(Filter::NotesAndRests) => is_general_note(found.element),
274 })
275 .collect();
276 self.run_over(&elements)
277 }
278
279 pub fn run_over<'a>(&self, elements: &[Searched<'a>]) -> Result<Vec<SearchMatch<'a>>> {
286 if self.search.is_empty() {
287 return Err(empty_search());
288 }
289 let mut found = Vec::new();
290 if self.search.len() > elements.len() {
291 return Ok(found);
292 }
293 for (index, window) in elements.windows(self.search.len()).enumerate() {
294 let refused = window.iter().zip(&self.search).any(|(searched, term)| {
295 self.algorithms
296 .iter()
297 .find_map(|algorithm| algorithm.compare(searched.element, term))
298 == Some(false)
299 });
300 if !refused {
301 found.push(SearchMatch {
302 index,
303 elements: window.to_vec(),
304 });
305 }
306 }
307 Ok(found)
308 }
309}
310
311fn empty_search() -> Error {
312 Error::Search("the search Stream or list cannot be empty".to_string())
313}
314
315fn search_with(
318 elements: &[Searched<'_>],
319 search: &[SearchTerm],
320 matches: impl Fn(&StreamElement, &SearchTerm) -> bool,
321) -> Result<Vec<usize>> {
322 if search.is_empty() {
323 return Err(empty_search());
324 }
325 if search.len() > elements.len() {
326 return Ok(Vec::new());
327 }
328 Ok(elements
329 .windows(search.len())
330 .enumerate()
331 .filter(|(_, window)| {
332 window
333 .iter()
334 .zip(search)
335 .all(|(searched, term)| matches(searched.element, term))
336 })
337 .map(|(index, _)| index)
338 .collect())
339}
340
341pub fn rhythmic_search(elements: &[Searched<'_>], search: &[SearchTerm]) -> Result<Vec<usize>> {
349 search_with(elements, search, |element, term| {
350 term.any_length()
351 || term
352 .element()
353 .is_some_and(|wanted| wanted.quarter_length() == element.quarter_length())
354 })
355}
356
357fn same_name(element: &StreamElement, term: &SearchTerm) -> bool {
358 matches!(
359 (term.element().and_then(name), name(element)),
360 (Some(wanted), Some(found)) if wanted == found
361 )
362}
363
364pub fn note_name_search(elements: &[Searched<'_>], search: &[SearchTerm]) -> Result<Vec<usize>> {
372 search_with(elements, search, |element, term| {
373 matches!(term, SearchTerm::Wildcard) || same_name(element, term)
374 })
375}
376
377pub fn note_name_rhythmic_search(
384 elements: &[Searched<'_>],
385 search: &[SearchTerm],
386) -> Result<Vec<usize>> {
387 search_with(elements, search, |element, term| {
388 if matches!(term, SearchTerm::Wildcard) {
389 return true;
390 }
391 same_name(element, term)
392 && (term.any_length()
393 || term
394 .element()
395 .is_some_and(|wanted| wanted.quarter_length() == element.quarter_length()))
396 })
397}
398
399fn first_pitch(element: &StreamElement) -> Option<Pitch> {
402 match element {
403 StreamElement::Note(note) => Some(note.pitch().clone()),
404 StreamElement::Chord(chord) => chord.pitches().into_iter().next(),
405 StreamElement::ChordSymbol(symbol) => symbol.pitches().ok()?.into_iter().next(),
406 _ => element.pitches().into_iter().next(),
407 }
408}
409
410fn tie_of(element: &StreamElement) -> Option<&Tie> {
412 match element {
413 StreamElement::Note(note) => note.tie(),
414 StreamElement::Chord(chord) => chord.tie(),
415 StreamElement::Rest(rest) => rest.tie(),
416 StreamElement::Unpitched(stroke) => stroke.written().tie(),
417 StreamElement::PercussionChord(chord) => chord.written().tie(),
418 _ => None,
419 }
420}
421
422pub fn translate_note_to_byte(element: &StreamElement) -> char {
426 let midi = match element {
427 StreamElement::Note(note) => Some(note.pitch().midi()),
428 StreamElement::Chord(_) | StreamElement::ChordSymbol(_) => {
429 first_pitch(element).map(|pitch| pitch.midi())
430 }
431 _ => None,
432 };
433 midi.and_then(|midi| u32::try_from(midi).ok())
434 .and_then(char::from_u32)
435 .unwrap_or('\u{7f}')
436}
437
438pub fn translate_duration_to_bytes(element: &StreamElement) -> char {
442 let length = element.quarter_length();
443 let mut code = 1;
444 if length != 0.0 {
445 code = ((length * 256.0).log2() * 10.0) as i64;
446 code = code.clamp(1, 127);
447 }
448 char::from_u32(code as u32).unwrap_or('\u{1}')
449}
450
451pub fn translate_note_tie_to_byte(element: &StreamElement) -> &'static str {
455 match tie_of(element).map(Tie::tie_type) {
456 Some(TieType::Start) => "s",
457 Some(TieType::Continue) => "c",
458 Some(TieType::Stop) => "e",
459 _ => "",
460 }
461}
462
463pub fn translate_note_with_duration_to_bytes(element: &StreamElement, include_tie: bool) -> String {
466 let mut text = String::new();
467 text.push(translate_note_to_byte(element));
468 text.push(translate_duration_to_bytes(element));
469 if include_tie {
470 text.push_str(translate_note_tie_to_byte(element));
471 }
472 text
473}
474
475#[derive(Clone, Debug, PartialEq, Eq)]
478pub struct Translation {
479 pub text: String,
481 pub measures: Vec<Option<IntegerType>>,
484}
485
486pub fn translate_stream_to_string(elements: &[Searched<'_>]) -> Translation {
489 Translation {
490 text: elements
491 .iter()
492 .map(|found| translate_note_with_duration_to_bytes(found.element, true))
493 .collect(),
494 measures: elements.iter().map(|found| found.measure).collect(),
495 }
496}
497
498pub fn translate_stream_to_string_no_rhythm(elements: &[Searched<'_>]) -> Translation {
501 Translation {
502 text: elements
503 .iter()
504 .map(|found| translate_note_to_byte(found.element))
505 .collect(),
506 measures: elements.iter().map(|found| found.measure).collect(),
507 }
508}
509
510pub fn translate_stream_to_string_only_rhythm(elements: &[Searched<'_>]) -> Translation {
513 Translation {
514 text: elements
515 .iter()
516 .map(|found| translate_duration_to_bytes(found.element))
517 .collect(),
518 measures: elements.iter().map(|found| found.measure).collect(),
519 }
520}
521
522fn translate_lines(
528 elements: &[Searched<'_>],
529 rest: char,
530 mut note: impl FnMut(&Pitch, usize) -> char,
531) -> Result<Translation> {
532 let mut text = String::new();
533 let mut measures = Vec::new();
534 let mut previous_rest = false;
535 let mut previous_tie = false;
536 let mut previous_length: Option<FloatType> = None;
537 for found in elements {
538 if matches!(found.element, StreamElement::Rest(_)) {
539 if !previous_rest {
540 previous_rest = true;
541 text.push(rest);
542 measures.push(found.measure);
543 }
544 continue;
545 }
546 previous_rest = false;
547 let tie = tie_of(found.element);
548 if previous_tie {
549 if tie.is_none_or(|tie| tie.tie_type() == TieType::Stop) {
550 previous_tie = false;
551 }
552 continue;
553 } else if tie.is_some() {
554 previous_tie = true;
555 }
556 let length = found.element.quarter_length();
557 let change = match previous_length {
558 None => 0,
559 Some(previous) if previous == length => 0,
560 Some(previous) if previous > length => 1,
561 Some(_) => 2,
562 };
563 previous_length = Some(length);
564 let pitch = first_pitch(found.element)
565 .ok_or_else(|| Error::Search("list index out of range".to_string()))?;
566 text.push(note(&pitch, change));
567 measures.push(found.measure);
568 }
569 Ok(Translation { text, measures })
570}
571
572pub fn translate_diatonic_stream_to_string(elements: &[Searched<'_>]) -> Result<Translation> {
582 translate_lines(elements, 'Z', |pitch, change| {
583 let shift = [0, 14, 7][change];
584 char::from_u32(pitch.step().as_char() as u32 + shift).unwrap_or('?')
585 })
586}
587
588pub fn translate_intervals_and_speed(elements: &[Searched<'_>]) -> Result<Translation> {
598 let mut previous_midi = elements
599 .iter()
600 .find_map(|found| match found.element {
601 StreamElement::Note(note) => Some(note.pitch().midi()),
602 _ => None,
603 })
604 .unwrap_or(60);
605 translate_lines(elements, ' ', |pitch, change| {
606 let shift = [27 + 14, 27 * 2 + 14, 14][change];
607 let midi = pitch.midi();
608 let difference = (previous_midi - midi).clamp(-13, 13);
609 previous_midi = midi;
610 char::from_u32((32 + difference + shift) as u32).unwrap_or('?')
611 })
612}
613
614pub fn similarity(a: &str, b: &str) -> FloatType {
617 let a: Vec<char> = a.chars().collect();
618 let b: Vec<char> = b.chars().collect();
619 difflib::ratio(&a, &b)
620}
621
622fn with_flat_notes<R>(stream: &Stream, read: impl FnOnce(&[Searched<'_>]) -> R) -> R {
626 let flat = stream.flatten();
627 let notes: Vec<Searched<'_>> = iterated(&flat)
628 .into_iter()
629 .filter(|found| is_general_note(found.element))
630 .collect();
631 read(¬es)
632}
633
634fn ranked(mut ratios: Vec<(usize, FloatType)>) -> Vec<(usize, FloatType)> {
637 ratios.sort_by(|a, b| (1.0 - a.1).total_cmp(&(1.0 - b.1)));
638 ratios
639}
640
641pub fn approximate_note_search(this: &Stream, others: &[&Stream]) -> Vec<(usize, FloatType)> {
646 approximate_by(this, others, |elements| {
647 translate_stream_to_string(elements).text
648 })
649}
650
651pub fn approximate_note_search_no_rhythm(
653 this: &Stream,
654 others: &[&Stream],
655) -> Vec<(usize, FloatType)> {
656 approximate_by(this, others, |elements| {
657 translate_stream_to_string_no_rhythm(elements).text
658 })
659}
660
661pub fn approximate_note_search_only_rhythm(
663 this: &Stream,
664 others: &[&Stream],
665) -> Vec<(usize, FloatType)> {
666 approximate_by(this, others, |elements| {
667 translate_stream_to_string_only_rhythm(elements).text
668 })
669}
670
671fn approximate_by(
672 this: &Stream,
673 others: &[&Stream],
674 translate: impl Fn(&[Searched<'_>]) -> String,
675) -> Vec<(usize, FloatType)> {
676 let this_text = with_flat_notes(this, &translate);
677 ranked(
678 others
679 .iter()
680 .enumerate()
681 .map(|(index, other)| {
682 let other_text = with_flat_notes(other, &translate);
683 (index, similarity(&this_text, &other_text))
684 })
685 .collect(),
686 )
687}
688
689pub fn approximate_note_search_weighted(
692 this: &Stream,
693 others: &[&Stream],
694) -> Vec<(usize, FloatType)> {
695 let texts = |notes: &[Searched<'_>]| {
696 (
697 translate_stream_to_string_no_rhythm(notes).text,
698 translate_stream_to_string_only_rhythm(notes).text,
699 )
700 };
701 let (pitches, lengths) = with_flat_notes(this, texts);
702 ranked(
703 others
704 .iter()
705 .enumerate()
706 .map(|(index, other)| {
707 let (other_pitches, other_lengths) = with_flat_notes(other, texts);
708 let by_pitch = similarity(&pitches, &other_pitches);
709 let by_length = similarity(&lengths, &other_lengths);
710 (index, (3.0 * by_pitch + by_length) / 4.0)
711 })
712 .collect(),
713 )
714}
715
716#[derive(Clone, Debug)]
718pub struct MeasureRhythm<'a> {
719 pub count: usize,
721 pub rhythm: String,
723 pub measures: Vec<&'a Stream>,
725}
726
727pub fn most_common_measure_rhythms(stream: &Stream) -> Vec<MeasureRhythm<'_>> {
732 let mut rhythms: Vec<MeasureRhythm<'_>> = Vec::new();
733 for found in recursed(stream) {
734 let StreamElement::Stream(measure) = found.element else {
735 continue;
736 };
737 if measure.kind() != StreamKind::Measure {
738 continue;
739 }
740 let own: Vec<Searched<'_>> = iterated(measure)
741 .into_iter()
742 .filter(|found| is_general_note(found.element))
743 .collect();
744 let rhythm = translate_stream_to_string_only_rhythm(&own).text;
745 match rhythms.iter_mut().find(|known| known.rhythm == rhythm) {
746 Some(known) => {
747 known.count += 1;
748 known.measures.push(measure);
749 }
750 None => rhythms.push(MeasureRhythm {
751 count: 1,
752 rhythm,
753 measures: vec![measure],
754 }),
755 }
756 }
757 rhythms.sort_by_key(|rhythm| std::cmp::Reverse(rhythm.count));
758 rhythms
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764 use crate::note::Note;
765 use crate::tinynotation::from_tiny_notation;
766
767 fn note(name: &str, length: FloatType) -> SearchTerm {
768 SearchTerm::Element(StreamElement::Note(
769 Note::from_name(name)
770 .expect("a note")
771 .with_duration(crate::duration::Duration::new(length).expect("a length")),
772 ))
773 }
774
775 #[test]
776 fn runs_are_found_by_rhythm_and_name() -> Result<()> {
777 let line = from_tiny_notation("3/4 c4. d8 e4 g4. a8 f4 c'8 d'4.")?;
780 let notes = notes_and_rests(&line);
781 assert_eq!(
782 rhythmic_search(¬es, &[note("C4", 1.5), note("C4", 0.5)])?,
783 [0, 3]
784 );
785 assert_eq!(
786 note_name_search(
787 ¬es,
788 &[note("D4", 1.0), SearchTerm::Wildcard, note("G4", 1.0)]
789 )?,
790 [1]
791 );
792 let mut searcher = StreamSearcher::new(vec![
793 note("D4", 0.5),
794 SearchTerm::Wildcard,
795 SearchTerm::AnyLength(note_element("G4")),
796 ]);
797 searcher.recurse = true;
798 searcher.filter = Some(Filter::Notes);
799 searcher.algorithms.push(Algorithm::Rhythm);
800 let found = searcher.run(&line)?;
801 assert_eq!(
802 found.iter().map(|found| found.index).collect::<Vec<_>>(),
803 [1, 4]
804 );
805 assert!(StreamSearcher::new(Vec::new()).run(&line).is_err());
806 Ok(())
807 }
808
809 fn note_element(name: &str) -> StreamElement {
810 StreamElement::Note(Note::from_name(name).expect("a note"))
811 }
812
813 #[test]
814 fn notes_are_translated_as_music21_translates_them() -> Result<()> {
815 let line = from_tiny_notation("3/4 c4 d8 r8 r4 e4~ e8 f8")?;
817 let notes = notes_and_rests(&line);
818 assert_eq!(
819 translate_stream_to_string(¬es).text,
820 "<P>F\u{7f}F\u{7f}P@Ps@FeAF"
821 );
822 assert_eq!(translate_diatonic_stream_to_string(¬es)?.text, "CRZLT");
823 assert_eq!(translate_intervals_and_speed(¬es)?.text, "Ib ,c");
824 Ok(())
825 }
826}