Skip to main content

music21_rs/analysis/
transposition.rs

1//! How many distinct transpositions a set of pitches has: music21's
2//! `analysis.transposition`.
3//!
4//! A set is transposed up by each of the twelve semitones, and the
5//! transpositions are told apart by their normal orders. An augmented
6//! triad has four, since every fourth transposition lands on itself:
7//!
8//! ```text
9//!   +0 [0,4,8]  +1 [1,5,9]  +2 [2,6,10]  +3 [3,7,11]  +4 [0,4,8] ...
10//! ```
11
12use crate::{
13    chord::Chord,
14    defaults::IntegerType,
15    error::{Error, Result},
16    interval::Interval,
17    pitch::Pitch,
18};
19
20/// The semitones in an octave, and so the transpositions there are.
21const SEMITONES: IntegerType = 12;
22
23/// A set of pitches and its transpositions: music21's
24/// `TranspositionChecker`.
25#[derive(Clone, Debug, PartialEq)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub struct TranspositionChecker {
28    pitches: Vec<Pitch>,
29}
30
31impl TranspositionChecker {
32    /// A checker of `pitches`.
33    ///
34    /// # Errors
35    ///
36    /// No pitches, which music21 refuses too.
37    pub fn new(pitches: Vec<Pitch>) -> Result<Self> {
38        if pitches.is_empty() {
39            return Err(Error::Value(
40                "Must have at least one element in list".to_string(),
41            ));
42        }
43        Ok(Self { pitches })
44    }
45
46    /// The pitches checked.
47    pub fn pitches(&self) -> &[Pitch] {
48        &self.pitches
49    }
50
51    /// The set moved up by each of 0 to 11 semitones, spelled as music21
52    /// spells a pitch moved by a number: music21's `getTranspositions`.
53    ///
54    /// # Errors
55    ///
56    /// A pitch that cannot be moved.
57    pub fn transpositions(&self) -> Result<Vec<Vec<Pitch>>> {
58        (0..SEMITONES)
59            .map(|semitones| {
60                let interval = Interval::from_semitones(semitones)?;
61                self.pitches
62                    .iter()
63                    .map(|pitch| pitch.transpose(&interval))
64                    .collect()
65            })
66            .collect()
67    }
68
69    /// The normal order of each transposition: music21's
70    /// `listNormalOrders`.
71    ///
72    /// # Errors
73    ///
74    /// A pitch that cannot be moved.
75    pub fn normal_orders(&self) -> Result<Vec<Vec<u8>>> {
76        self.transpositions()?
77            .iter()
78            .map(|pitches| Ok(Chord::new(pitches.as_slice())?.normal_order()))
79            .collect()
80    }
81
82    /// The normal orders, each once, in the order first met: music21's
83    /// `listDistinctNormalOrders`.
84    ///
85    /// # Errors
86    ///
87    /// A pitch that cannot be moved.
88    pub fn distinct_normal_orders(&self) -> Result<Vec<Vec<u8>>> {
89        let mut distinct: Vec<Vec<u8>> = Vec::new();
90        for order in self.normal_orders()? {
91            if distinct.contains(&order) {
92                continue;
93            }
94            distinct.push(order);
95        }
96        Ok(distinct)
97    }
98
99    /// How many distinct transpositions the set has: music21's
100    /// `numDistinctTranspositions`.
101    ///
102    /// ```
103    /// use music21_rs::{Pitch, analysis::transposition::TranspositionChecker};
104    ///
105    /// let augmented = ["C4", "E4", "G#4"].iter().map(|n| n.parse()).collect::<Result<_, _>>()?;
106    /// assert_eq!(TranspositionChecker::new(augmented)?.distinct_count()?, 4);
107    /// # Ok::<(), music21_rs::Error>(())
108    /// ```
109    ///
110    /// # Errors
111    ///
112    /// A pitch that cannot be moved.
113    pub fn distinct_count(&self) -> Result<usize> {
114        Ok(self.distinct_normal_orders()?.len())
115    }
116
117    /// A chord of each distinct normal order, spelled as music21 spells a
118    /// chord of pitch-class numbers: music21's
119    /// `getChordsOfDistinctTranspositions`.
120    ///
121    /// # Errors
122    ///
123    /// A pitch that cannot be moved.
124    pub fn distinct_chords(&self) -> Result<Vec<Chord>> {
125        self.distinct_normal_orders()?
126            .into_iter()
127            .map(|order| {
128                let classes: Vec<IntegerType> = order.into_iter().map(IntegerType::from).collect();
129                Chord::new(classes.as_slice())
130            })
131            .collect()
132    }
133
134    /// The pitches of each of those chords: music21's
135    /// `getPitchesOfDistinctTranspositions`.
136    ///
137    /// # Errors
138    ///
139    /// A pitch that cannot be moved.
140    pub fn distinct_pitches(&self) -> Result<Vec<Vec<Pitch>>> {
141        Ok(self.distinct_chords()?.iter().map(Chord::pitches).collect())
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    fn checker(names: &[&str]) -> TranspositionChecker {
150        TranspositionChecker::new(names.iter().map(|name| name.parse().unwrap()).collect()).unwrap()
151    }
152
153    fn names(pitches: &[Pitch]) -> Vec<String> {
154        pitches.iter().map(Pitch::name).collect()
155    }
156
157    #[test]
158    fn a_pitch_moves_through_music21s_spellings() {
159        let moved: Vec<String> = checker(&["D#"])
160            .transpositions()
161            .unwrap()
162            .iter()
163            .map(|pitches| pitches[0].name())
164            .collect();
165        assert_eq!(
166            moved,
167            [
168                "Eb", "E", "F", "F#", "G", "G#", "A", "Bb", "B", "C", "C#", "D"
169            ]
170        );
171    }
172
173    #[test]
174    fn an_augmented_triad_has_four_transpositions() {
175        let augmented = checker(&["C4", "E4", "G#4"]);
176        let orders = augmented.normal_orders().unwrap();
177        assert_eq!(orders.len(), 12);
178        assert_eq!(orders[4], [0, 4, 8]);
179        assert_eq!(
180            augmented.distinct_normal_orders().unwrap(),
181            [vec![0, 4, 8], vec![1, 5, 9], vec![2, 6, 10], vec![3, 7, 11]]
182        );
183        assert_eq!(augmented.distinct_count().unwrap(), 4);
184
185        let pitches: Vec<Vec<String>> = augmented
186            .distinct_pitches()
187            .unwrap()
188            .iter()
189            .map(|pitches| names(pitches))
190            .collect();
191        assert_eq!(
192            pitches,
193            [
194                ["C", "E", "G#"],
195                ["C#", "F", "A"],
196                ["D", "F#", "A#"],
197                ["Eb", "G", "B"],
198            ]
199        );
200    }
201
202    #[test]
203    fn a_diminished_seventh_has_three() {
204        let chords = checker(&["C", "Eb", "F#", "A"]).distinct_chords().unwrap();
205        let spelled: Vec<Vec<String>> = chords.iter().map(|chord| chord.pitch_names()).collect();
206        assert_eq!(
207            spelled,
208            [
209                ["C", "Eb", "F#", "A"],
210                ["C#", "E", "G", "A#"],
211                ["D", "F", "G#", "B"],
212            ]
213        );
214    }
215
216    #[test]
217    fn no_pitches_are_refused() {
218        assert!(TranspositionChecker::new(Vec::new()).is_err());
219        assert_eq!(checker(&["D#"]).pitches().len(), 1);
220    }
221}