Skip to main content

music21_rs/search/
segment.rs

1//! Parts cut into overlapping stretches of text, and every stretch of some
2//! scores compared with every other: music21's `search.segment`.
3
4use crate::{
5    defaults::IntegerType,
6    error::{Error, Result},
7    stream::{Stream, StreamElement},
8};
9
10use super::{Searched, Translation, difflib, recursed, translate_stream_to_string_no_rhythm};
11
12/// A part cut into stretches of its translated notes, and the measures
13/// each stretch runs from and to: one entry of music21's score index.
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct Segments {
16    /// Each stretch of text.
17    pub segments: Vec<String>,
18    /// The measure of each stretch's first and last character.
19    pub measures: Vec<(Option<IntegerType>, Option<IntegerType>)>,
20}
21
22/// The notes of a stream in the order music21's `recurse().notes.stream()`
23/// leaves them: by offset in the stream, then by class, grace notes first,
24/// then as the walk met them -- so the notes of two voices interleave.
25fn sorted_notes(stream: &Stream) -> Vec<Searched<'_>> {
26    let mut notes: Vec<Searched<'_>> = recursed(stream)
27        .into_iter()
28        .filter(|found| {
29            matches!(
30                found.element,
31                StreamElement::Note(_)
32                    | StreamElement::Chord(_)
33                    | StreamElement::Unpitched(_)
34                    | StreamElement::PercussionChord(_)
35                    | StreamElement::ChordSymbol(_)
36            )
37        })
38        .collect();
39    let grace = |element: &StreamElement| {
40        element
41            .duration()
42            .is_some_and(crate::duration::Duration::is_grace)
43    };
44    notes.sort_by(|left, right| {
45        left.offset
46            .total_cmp(&right.offset)
47            .then_with(|| {
48                left.element
49                    .class_sort_order()
50                    .cmp(&right.element.class_sort_order())
51            })
52            .then_with(|| grace(right.element).cmp(&grace(left.element)))
53    });
54    notes
55}
56
57/// A part's notes, written by `translate`, cut into stretches
58/// `segment_length` characters long, each starting `segment_length -
59/// overlap` after the one before, the last as long as what is left:
60/// music21's `translateMonophonicPartToSegments`. Each stretch's measures
61/// are read off the translation's measures at its first and last
62/// character, as music21 reads them, which is a note's measure only where
63/// the translation writes one character a note, as
64/// [`translate_stream_to_string_no_rhythm`], music21's default, does.
65///
66/// music21's `jitter`, which moves each start by a random amount, is not
67/// offered.
68///
69/// # Errors
70///
71/// An overlap as long as the stretches, or a translation with fewer
72/// measures than characters, which music21 cannot index either.
73pub fn translate_monophonic_part_to_segments(
74    part: &Stream,
75    segment_length: usize,
76    overlap: usize,
77    translate: fn(&[Searched<'_>]) -> Translation,
78) -> Result<Segments> {
79    let step = segment_length
80        .checked_sub(overlap)
81        .filter(|step| *step > 0)
82        .ok_or_else(|| Error::Search("the overlap must be shorter than a segment".to_string()))?;
83    let notes = sorted_notes(part);
84    let translation = translate(&notes);
85    let text: Vec<char> = translation.text.chars().collect();
86    let total = text.len();
87    let count = total.div_ceil(step);
88    let measure_at = |index: usize| -> Result<Option<IntegerType>> {
89        translation
90            .measures
91            .get(index)
92            .copied()
93            .ok_or_else(|| Error::Search("list index out of range".to_string()))
94    };
95    let mut segments = Segments {
96        segments: Vec::with_capacity(count),
97        measures: Vec::with_capacity(count),
98    };
99    for number in 0..count {
100        let start = (number * step).min(total - 1);
101        let end = (start + segment_length).min(total);
102        segments.segments.push(text[start..end].iter().collect());
103        segments
104            .measures
105            .push((measure_at(start)?, measure_at(end - 1)?));
106    }
107    Ok(segments)
108}
109
110/// Each part of a score cut into segments, by music21's defaults --
111/// thirty characters a segment, twelve overlapping, pitches alone:
112/// music21's `indexScoreParts`.
113///
114/// # Errors
115///
116/// As [`translate_monophonic_part_to_segments`].
117pub fn index_score_parts(score: &Stream) -> Result<Vec<Segments>> {
118    index_score_parts_with(score, 30, 12, translate_stream_to_string_no_rhythm)
119}
120
121/// The same, cut as asked.
122///
123/// # Errors
124///
125/// As [`translate_monophonic_part_to_segments`].
126pub fn index_score_parts_with(
127    score: &Stream,
128    segment_length: usize,
129    overlap: usize,
130    translate: fn(&[Searched<'_>]) -> Translation,
131) -> Result<Vec<Segments>> {
132    score
133        .parts()
134        .into_iter()
135        .map(|part| translate_monophonic_part_to_segments(part, segment_length, overlap, translate))
136        .collect()
137}
138
139/// Where a segment comes from in an index of scores.
140#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct SegmentAt {
142    /// The score's name in the index.
143    pub score: String,
144    /// The part, counted from nought.
145    pub part: usize,
146    /// The segment of the part, counted from nought.
147    pub segment: usize,
148    /// The measures the segment runs from and to.
149    pub measures: (Option<IntegerType>, Option<IntegerType>),
150}
151
152/// How alike two segments are: one row of music21's `scoreSimilarity`.
153#[derive(Clone, Debug, PartialEq)]
154pub struct SegmentSimilarity {
155    /// The segment compared.
156    pub this: SegmentAt,
157    /// The segment it is compared with.
158    pub that: SegmentAt,
159    /// Python's `difflib` ratio of the two.
160    pub ratio: crate::defaults::FloatType,
161}
162
163/// Every segment of an index of named scores at least `minimum_length`
164/// characters long, compared with every such segment of each score after
165/// its own: music21's `scoreSimilarity`, by `difflib`. A score's segments
166/// are not compared with each other, as music21 compares none. With `include_reverse`, each comparison comes again with
167/// its two segments the other way round.
168pub fn score_similarity(
169    index: &[(String, Vec<Segments>)],
170    minimum_length: usize,
171    include_reverse: bool,
172) -> Vec<SegmentSimilarity> {
173    let long_enough = |segment: &str| segment.chars().count() >= minimum_length;
174    let at = |score: &str, part: usize, segment: usize, segments: &Segments| SegmentAt {
175        score: score.to_string(),
176        part,
177        segment,
178        measures: segments.measures[segment],
179    };
180    let mut rows = Vec::new();
181    for (this_number, (this_name, this_score)) in index.iter().enumerate() {
182        for (this_part, this_segments) in this_score.iter().enumerate() {
183            for (this_segment, this_text) in this_segments.segments.iter().enumerate() {
184                if !long_enough(this_text) {
185                    continue;
186                }
187                let this_chars: Vec<char> = this_text.chars().collect();
188                let this_at = at(this_name, this_part, this_segment, this_segments);
189                for (that_name, that_score) in &index[this_number + 1..] {
190                    for (that_part, that_segments) in that_score.iter().enumerate() {
191                        for (that_segment, that_text) in that_segments.segments.iter().enumerate() {
192                            if !long_enough(that_text) {
193                                continue;
194                            }
195                            let that_chars: Vec<char> = that_text.chars().collect();
196                            // music21 sets this segment as difflib's second
197                            // sequence and each other as its first.
198                            let ratio = difflib::ratio(&that_chars, &this_chars);
199                            let that_at = at(that_name, that_part, that_segment, that_segments);
200                            rows.push(SegmentSimilarity {
201                                this: this_at.clone(),
202                                that: that_at.clone(),
203                                ratio,
204                            });
205                            if include_reverse {
206                                rows.push(SegmentSimilarity {
207                                    this: that_at,
208                                    that: this_at.clone(),
209                                    ratio,
210                                });
211                            }
212                        }
213                    }
214                }
215            }
216        }
217    }
218    rows
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use crate::tinynotation::from_tiny_notation;
225
226    #[test]
227    fn a_part_is_cut_into_overlapping_segments() -> Result<()> {
228        // music21: translateMonophonicPartToSegments of the same line, with
229        // segmentLengths=4 and overlap=2.
230        let line = from_tiny_notation("4/4 c4 d e f g a b c' d' e' f' g'")?;
231        let cut = translate_monophonic_part_to_segments(
232            &line,
233            4,
234            2,
235            translate_stream_to_string_no_rhythm,
236        )?;
237        assert_eq!(cut.segments, ["<>@A", "@ACE", "CEGH", "GHJL", "JLMO", "MO"]);
238        assert_eq!(
239            cut.measures,
240            [
241                (Some(1), Some(1)),
242                (Some(1), Some(2)),
243                (Some(2), Some(2)),
244                (Some(2), Some(3)),
245                (Some(3), Some(3)),
246                (Some(3), Some(3)),
247            ]
248        );
249        assert!(
250            translate_monophonic_part_to_segments(
251                &line,
252                4,
253                4,
254                translate_stream_to_string_no_rhythm
255            )
256            .is_err()
257        );
258        Ok(())
259    }
260}