music21_rs/analysis/
transposition.rs1use crate::{
13 chord::Chord,
14 defaults::IntegerType,
15 error::{Error, Result},
16 interval::Interval,
17 pitch::Pitch,
18};
19
20const SEMITONES: IntegerType = 12;
22
23#[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 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 pub fn pitches(&self) -> &[Pitch] {
48 &self.pitches
49 }
50
51 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 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 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 pub fn distinct_count(&self) -> Result<usize> {
114 Ok(self.distinct_normal_orders()?.len())
115 }
116
117 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 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}