Skip to main content

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