Skip to main content

music21_rs/analysis/
correlate.rs

1//! How what a stream's notes sound goes with the dynamics they sound under:
2//! music21's `analysis.correlate`.
3
4use crate::{
5    defaults::FloatType,
6    dynamics::SHORT_NAMES,
7    error::{Error, Result},
8    pitch::Pitch,
9    stream::{Stream, StreamElement},
10};
11
12/// Each pitch of each note and chord of a stream flattened, with each
13/// dynamic it sounds under -- the dynamic's place in [`SHORT_NAMES`], from
14/// `pppppp` at nought to `ffffff` -- one pair for each: music21's
15/// `ActivityMatch.pitchToDynamic`. A dynamic holds from its offset to the
16/// next dynamic's, the last to the end of the stream, and a note starting
17/// just where one dynamic ends and the next begins sounds under both.
18/// music21 reads where a dynamic ends as its offset plus its length in
19/// Python's arithmetic, which rounds a sum of an offset in quarters and a
20/// length in thirds, so a note on a third of a beat can fall just after
21/// the end and sound under the next dynamic alone; this reads them so too.
22///
23/// # Errors
24///
25/// A stream with no notes or chords, or no dynamics, or a dynamic not one
26/// of [`SHORT_NAMES`], such as `sfz`, which music21 cannot place either.
27pub fn pitch_to_dynamic(stream: &Stream) -> Result<Vec<(FloatType, usize)>> {
28    let flat = stream.flatten();
29    let events = flat.events();
30    let notes: Vec<(FloatType, Vec<Pitch>)> = events
31        .iter()
32        .filter_map(|event| match event.element() {
33            StreamElement::Note(note) => Some((event.offset(), vec![note.pitch().clone()])),
34            StreamElement::Chord(chord) => Some((event.offset(), chord.pitches())),
35            StreamElement::ChordSymbol(symbol) => {
36                Some((event.offset(), symbol.pitches().unwrap_or_default()))
37            }
38            _ => None,
39        })
40        .collect();
41    if notes.is_empty() {
42        return Err(missing(
43            "(<class 'music21.note.Note'>, <class 'music21.chord.Chord'>)",
44        ));
45    }
46    let dynamics: Vec<(FloatType, &str)> = events
47        .iter()
48        .filter_map(|event| match event.element() {
49            StreamElement::Dynamic(dynamic) => Some((event.offset(), dynamic.value())),
50            _ => None,
51        })
52        .collect();
53    if dynamics.is_empty() {
54        return Err(missing("<class 'music21.dynamics.Dynamic'>"));
55    }
56    let end = Offset::of(flat.end_offset());
57    let mut under: Vec<Vec<usize>> = vec![Vec::new(); notes.len()];
58    for (index, (start, value)) in dynamics.iter().enumerate() {
59        let start = Offset::of(*start);
60        let next = dynamics
61            .get(index + 1)
62            .map_or(end, |(next, _)| Offset::of(*next));
63        // music21 lengthens each dynamic to the next and reads where it
64        // stops as its offset plus that length, in Python's arithmetic.
65        let stop = start.plus(next.minus(start).op_frac());
66        let level = SHORT_NAMES
67            .iter()
68            .position(|name| name == value)
69            .ok_or_else(|| Error::Analysis(format!("'{value}' is not in list")))?;
70        for ((offset, _), heard) in notes.iter().zip(&mut under) {
71            let offset = Offset::of(*offset);
72            if start.at_most(offset) && offset.at_most(stop) {
73                heard.push(level);
74            }
75        }
76    }
77    Ok(notes
78        .iter()
79        .zip(&under)
80        .flat_map(|((_, pitches), heard)| {
81            pitches
82                .iter()
83                .flat_map(move |pitch| heard.iter().map(move |level| (pitch.ps(), *level)))
84        })
85        .collect())
86}
87
88/// The same pairs, each once, with how many times it comes, in the order
89/// first met: music21's `pitchToDynamic(dataPoints=False)`.
90///
91/// # Errors
92///
93/// As [`pitch_to_dynamic`].
94pub fn pitch_to_dynamic_counts(stream: &Stream) -> Result<Vec<(FloatType, usize, usize)>> {
95    let mut counts: Vec<(FloatType, usize, usize)> = Vec::new();
96    for (pitch, level) in pitch_to_dynamic(stream)? {
97        match counts
98            .iter_mut()
99            .find(|(known, known_level, _)| *known == pitch && *known_level == level)
100        {
101            Some((_, _, count)) => *count += 1,
102            None => counts.push((pitch, level, 1)),
103        }
104    }
105    Ok(counts)
106}
107
108/// An offset as music21 holds it: a float where its denominator is a
109/// power of two, an exact fraction where not, which Python adds and
110/// compares by its own rules.
111#[derive(Clone, Copy, Debug)]
112enum Offset {
113    Float(FloatType),
114    Fraction(i128, i128),
115}
116
117/// A float's exact value as a fraction over a power of two: Python's
118/// `float.as_integer_ratio`.
119fn integer_ratio(value: FloatType) -> (i128, i128) {
120    if value == 0.0 {
121        return (0, 1);
122    }
123    let bits = value.to_bits();
124    let sign = if bits >> 63 == 1 { -1 } else { 1 };
125    let exponent = ((bits >> 52) & 0x7ff) as i32;
126    let mut mantissa = (bits & ((1 << 52) - 1)) as i128;
127    let mut power = if exponent == 0 {
128        -1074
129    } else {
130        mantissa |= 1 << 52;
131        exponent - 1075
132    };
133    while mantissa % 2 == 0 && power < 0 {
134        mantissa /= 2;
135        power += 1;
136    }
137    if power >= 0 {
138        (sign * (mantissa << power), 1)
139    } else {
140        (sign * mantissa, 1 << -power)
141    }
142}
143
144fn gcd(a: i128, b: i128) -> i128 {
145    if b == 0 { a.abs() } else { gcd(b, a % b) }
146}
147
148fn power_of_two(denominator: i128) -> bool {
149    denominator > 0 && denominator & (denominator - 1) == 0
150}
151
152impl Offset {
153    /// The offset music21 holds where the crate holds this float: music21's
154    /// `opFrac` of it.
155    fn of(value: FloatType) -> Self {
156        Self::Float(value).op_frac()
157    }
158
159    /// music21's `opFrac`: a float whose exact denominator is above 65535
160    /// read as the nearest fraction with one that is not, and a fraction
161    /// over a power of two made a float.
162    fn op_frac(self) -> Self {
163        match self {
164            Self::Float(value) => {
165                let (_, denominator) = integer_ratio(value);
166                if denominator <= 65535 {
167                    return self;
168                }
169                match crate::duration::limited_fraction(value, 65535) {
170                    Some((numerator, denominator)) if !power_of_two(denominator) => {
171                        Self::Fraction(numerator, denominator)
172                    }
173                    Some((numerator, denominator)) => {
174                        Self::Float(numerator as FloatType / denominator as FloatType)
175                    }
176                    None => self,
177                }
178            }
179            Self::Fraction(numerator, denominator) if power_of_two(denominator) => {
180                Self::Float(numerator as FloatType / denominator as FloatType)
181            }
182            fraction => fraction,
183        }
184    }
185
186    fn as_float(self) -> FloatType {
187        match self {
188            Self::Float(value) => value,
189            Self::Fraction(numerator, denominator) => {
190                numerator as FloatType / denominator as FloatType
191            }
192        }
193    }
194
195    /// Python's sum: exact for two fractions, a float otherwise.
196    fn plus(self, other: Self) -> Self {
197        match (self, other) {
198            (Self::Fraction(a, b), Self::Fraction(c, d)) => {
199                let (numerator, denominator) = (a * d + c * b, b * d);
200                let divisor = gcd(numerator, denominator);
201                Self::Fraction(numerator / divisor, denominator / divisor)
202            }
203            _ => Self::Float(self.as_float() + other.as_float()),
204        }
205    }
206
207    fn minus(self, other: Self) -> Self {
208        let negated = match other {
209            Self::Float(value) => Self::Float(-value),
210            Self::Fraction(numerator, denominator) => Self::Fraction(-numerator, denominator),
211        };
212        self.plus(negated)
213    }
214
215    /// The exact value as a fraction.
216    fn exact(self) -> (i128, i128) {
217        match self {
218            Self::Float(value) => integer_ratio(value),
219            Self::Fraction(numerator, denominator) => (numerator, denominator),
220        }
221    }
222
223    /// Python's `<=`, which compares a float and a fraction exactly.
224    fn at_most(self, other: Self) -> bool {
225        let ((a, b), (c, d)) = (self.exact(), other.exact());
226        a * d <= c * b
227    }
228}
229
230fn missing(class: &str) -> Error {
231    Error::Analysis(format!(
232        "cannot create correlation: an object that is not found in the Stream: {class}"
233    ))
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use crate::dynamics::Dynamic;
240    use crate::tinynotation::from_tiny_notation;
241
242    #[test]
243    fn pitches_are_paired_with_the_dynamics_they_sound_under() -> Result<()> {
244        let mut line = from_tiny_notation("4/4 c4 d e f")?;
245        line.insert(0.0, Dynamic::new("p"));
246        line.insert(2.0, Dynamic::new("f"));
247        // music21: the note at 2.0 sounds under both, the p's span ending
248        // where the f begins.
249        assert_eq!(
250            pitch_to_dynamic(&line)?,
251            [(60.0, 5), (62.0, 5), (64.0, 5), (64.0, 8), (65.0, 8)]
252        );
253        assert_eq!(pitch_to_dynamic_counts(&line)?.len(), 5);
254        assert!(pitch_to_dynamic(&from_tiny_notation("4/4 c4")?).is_err());
255        Ok(())
256    }
257}