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(¬es)? - 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}