Skip to main content

music21_rs/analysis/
patel.rs

1//! Aniruddh D. Patel's measures of rhythm and melody: music21's
2//! `analysis.patel`.
3
4use crate::{
5    defaults::FloatType,
6    error::{Error, Result},
7    stream::{ConsecutiveOptions, Stream},
8};
9
10/// The normalized pairwise variability index of a stream's rhythm (Low,
11/// Grabe and Nolan, 2000): music21's `nPVI`.
12///
13/// Each element the stream holds itself is compared with the one before by
14/// how long it lasts -- the difference of the two lengths over their mean
15/// -- and the comparisons are averaged and scaled by a hundred. A pair in
16/// which either lasts no time adds nothing, but still counts. Every element
17/// counts, clefs and meters included, so a stream of the notes and rests
18/// alone is usually what is meant.
19///
20/// ```
21/// use music21_rs::Stream;
22/// use music21_rs::analysis::patel::n_pvi;
23/// use music21_rs::stream::{StreamElement, StreamEvent};
24/// use music21_rs::tinynotation::from_tiny_notation;
25///
26/// let line = from_tiny_notation("4/4 C4 D8 C4 D8 C4")?.flatten();
27/// let notes = Stream::from_events(line.events().iter().cloned().filter(|event| {
28///     matches!(event.element(), StreamElement::Note(_) | StreamElement::Rest(_))
29/// }));
30/// assert!((n_pvi(&notes)? - 66.666_666_666_666_67).abs() < 1e-9);
31/// # Ok::<(), music21_rs::Error>(())
32/// ```
33///
34/// # Errors
35///
36/// A stream holding fewer than two elements.
37pub fn n_pvi(stream: &Stream) -> Result<FloatType> {
38    let lengths: Vec<FloatType> = stream
39        .events()
40        .iter()
41        .map(|event| event.element().quarter_length())
42        .collect();
43    if lengths.len() < 2 {
44        return Err(Error::Analysis(
45            "the pairwise variability of a rhythm needs two elements".to_string(),
46        ));
47    }
48    let summation: FloatType = lengths
49        .windows(2)
50        .filter(|pair| pair[0] > 0.0 && pair[1] > 0.0)
51        .map(|pair| (pair[1] - pair[0]).abs() / ((pair[1] + pair[0]) / 2.0))
52        .sum();
53    Ok(summation * 100.0 / (lengths.len() - 1) as FloatType)
54}
55
56/// The melodic interval variability of a stream (Patel, *Music, Language,
57/// and the Brain*, p. 223): music21's `melodicIntervalVariability`.
58///
59/// It is a hundred times the coefficient of variation -- the sample
60/// standard deviation over the mean -- of the sizes in semitones of the
61/// intervals [`Stream::melodic_intervals`] finds with `options`, worked out
62/// exactly and rounded once, as Python's `statistics` module does.
63///
64/// ```
65/// use music21_rs::analysis::patel::melodic_interval_variability;
66/// use music21_rs::stream::ConsecutiveOptions;
67/// use music21_rs::tinynotation::from_tiny_notation;
68///
69/// let line = from_tiny_notation("4/4 C4 D E F G C")?;
70/// let miv = melodic_interval_variability(&line, &ConsecutiveOptions::default())?;
71/// assert!((miv - 85.266_688).abs() < 1e-6);
72/// # Ok::<(), music21_rs::Error>(())
73/// ```
74///
75/// # Errors
76///
77/// Fewer than two intervals, intervals averaging nothing, or as
78/// [`Stream::melodic_intervals`].
79pub fn melodic_interval_variability(
80    stream: &Stream,
81    options: &ConsecutiveOptions,
82) -> Result<FloatType> {
83    let sizes: Vec<FloatType> = stream
84        .melodic_intervals(options)?
85        .iter()
86        .map(|step| step.interval().chromatic().undirected())
87        .collect();
88    if sizes.len() < 2 {
89        return Err(Error::Analysis(
90            "need at least three notes to have a std-deviation of intervals (and thus a MIV)"
91                .to_string(),
92        ));
93    }
94    let (deviation, mean) = crate::statistics::deviation_and_mean(&sizes);
95    if mean == 0.0 {
96        return Err(Error::Analysis(
97            "the intervals of a line of one pitch have no variability".to_string(),
98        ));
99    }
100    Ok(100.0 * (deviation / mean))
101}