music21_rs/pitch/
pitchclass.rs1use crate::{
2 defaults::{FloatType, IntegerType, PITCH_SPACE_SIGNIFICANT_DIGITS},
3 error::{Error, Result},
4};
5
6use std::fmt::{Display, Formatter};
7use std::str::FromStr;
8
9#[derive(Clone, Debug, PartialEq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub enum PitchClassSpecifier {
13 Number(FloatType),
15 String(String),
17 PitchClass(PitchClass),
19}
20
21impl PitchClassSpecifier {
22 pub(crate) fn to_number(&self) -> Result<FloatType> {
23 match self {
24 Self::Number(value) => Ok(*value),
25 Self::String(value) => parse_pitch_class_string(value),
26 Self::PitchClass(pitch_class) => Ok(pitch_class.number()),
27 }
28 }
29}
30
31impl From<IntegerType> for PitchClassSpecifier {
32 fn from(value: IntegerType) -> Self {
33 Self::Number(value as FloatType)
34 }
35}
36
37impl From<u8> for PitchClassSpecifier {
38 fn from(value: u8) -> Self {
39 Self::Number(value as FloatType)
40 }
41}
42
43impl From<FloatType> for PitchClassSpecifier {
44 fn from(value: FloatType) -> Self {
45 Self::Number(value)
46 }
47}
48
49impl From<char> for PitchClassSpecifier {
50 fn from(value: char) -> Self {
51 Self::String(value.to_string())
52 }
53}
54
55impl From<&str> for PitchClassSpecifier {
56 fn from(value: &str) -> Self {
57 Self::String(value.to_string())
58 }
59}
60
61impl From<String> for PitchClassSpecifier {
62 fn from(value: String) -> Self {
63 Self::String(value)
64 }
65}
66
67impl From<PitchClass> for PitchClassSpecifier {
68 fn from(value: PitchClass) -> Self {
69 Self::PitchClass(value)
70 }
71}
72
73impl Display for PitchClassSpecifier {
74 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
75 match self {
76 Self::Number(number) => write!(f, "{number}"),
77 Self::String(value) => write!(f, "{value}"),
78 Self::PitchClass(pitch_class) => write!(f, "{pitch_class}"),
79 }
80 }
81}
82
83impl FromStr for PitchClass {
84 type Err = Error;
85
86 fn from_str(value: &str) -> Result<Self> {
87 Self::new(value)
88 }
89}
90
91impl TryFrom<&str> for PitchClass {
92 type Error = Error;
93
94 fn try_from(value: &str) -> Result<Self> {
95 Self::new(value)
96 }
97}
98
99impl TryFrom<String> for PitchClass {
100 type Error = Error;
101
102 fn try_from(value: String) -> Result<Self> {
103 Self::new(value)
104 }
105}
106
107impl TryFrom<char> for PitchClass {
108 type Error = Error;
109
110 fn try_from(value: char) -> Result<Self> {
111 Self::new(value)
112 }
113}
114
115impl TryFrom<IntegerType> for PitchClass {
116 type Error = Error;
117
118 fn try_from(value: IntegerType) -> Result<Self> {
119 Self::new(value)
120 }
121}
122
123impl TryFrom<u8> for PitchClass {
124 type Error = Error;
125
126 fn try_from(value: u8) -> Result<Self> {
127 Self::new(value)
128 }
129}
130
131impl TryFrom<FloatType> for PitchClass {
132 type Error = Error;
133
134 fn try_from(value: FloatType) -> Result<Self> {
135 Self::new(value)
136 }
137}
138
139#[derive(Clone, Copy, Debug, PartialEq)]
145#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
146#[must_use]
147pub struct PitchClass {
148 value: FloatType,
149}
150
151impl PitchClass {
152 pub fn new(specifier: impl Into<PitchClassSpecifier>) -> Result<Self> {
155 match specifier.into() {
156 PitchClassSpecifier::PitchClass(pitch_class) => Ok(pitch_class),
157 specifier => Self::from_number(specifier.to_number()?),
158 }
159 }
160
161 pub(crate) fn from_number(value: FloatType) -> Result<Self> {
162 if !value.is_finite() {
163 return Err(Error::PitchClass(format!(
164 "pitch class must be finite, got {value}"
165 )));
166 }
167
168 Ok(Self {
169 value: normalize_pitch_class(value),
170 })
171 }
172
173 pub fn number(&self) -> FloatType {
175 self.value
176 }
177
178 pub fn integer(&self) -> Option<IntegerType> {
180 if self.value.fract() == 0.0 {
181 Some(self.value as IntegerType)
182 } else {
183 None
184 }
185 }
186
187 pub fn string(&self) -> String {
189 pitch_class_to_string(self.value)
190 }
191}
192
193impl Display for PitchClass {
194 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
195 write!(f, "{}", self.string())
196 }
197}
198
199pub fn convert_pitch_class_to_str(pc: IntegerType) -> String {
202 pitch_class_char(pc).to_string()
203}
204
205pub(crate) fn pitch_class_char(pc: IntegerType) -> char {
213 const WRITTEN: [char; 12] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B'];
214 WRITTEN[pc.rem_euclid(12) as usize]
216}
217
218fn pitch_class_to_string(pc: FloatType) -> String {
219 let pc = normalize_pitch_class(pc);
220 if pc.fract() == 0.0 {
221 return convert_pitch_class_to_str(pc as IntegerType);
222 }
223
224 trim_float(pc)
225}
226
227fn normalize_pitch_class(pc: FloatType) -> FloatType {
228 let factor = (10 as FloatType).powi(PITCH_SPACE_SIGNIFICANT_DIGITS as IntegerType);
229 let normalized = (pc.rem_euclid(12.0) * factor).round() / factor;
230 if normalized == 12.0 { 0.0 } else { normalized }
231}
232
233fn parse_pitch_class_string(value: &str) -> Result<FloatType> {
234 let value = value.trim();
235 let mut letters = value.chars();
236 if let (Some(letter), None) = (letters.next(), letters.next())
237 && let Some(number) = pitch_class_letter_number(letter)
238 {
239 return Ok(number as FloatType);
240 }
241
242 value
243 .parse::<FloatType>()
244 .map_err(|err| Error::PitchClass(format!("cannot parse pitch class {value:?}: {err}")))
245}
246
247fn pitch_class_letter_number(letter: char) -> Option<IntegerType> {
250 match letter.to_ascii_lowercase() {
251 'a' | 't' => Some(10),
252 'b' | 'e' => Some(11),
253 _ => None,
254 }
255}
256
257fn trim_float(value: FloatType) -> String {
258 let text = format!("{value:.6}");
259 text.trim_end_matches('0').trim_end_matches('.').to_string()
260}
261
262pub(crate) fn convert_ps_to_oct(ps: FloatType) -> IntegerType {
263 let factor = (10 as FloatType).powi(PITCH_SPACE_SIGNIFICANT_DIGITS as IntegerType);
264 let ps_rounded = (ps * factor).round() / factor;
265 (ps_rounded / 12.0).floor() as IntegerType - 1
266}
267
268#[cfg(test)]
269mod tests {
270 #[test]
271 fn a_pitch_class_is_read_from_a_number_a_name_or_a_string() {
272 use super::{PitchClass, PitchClassSpecifier};
273
274 assert_eq!(PitchClass::try_from("A").unwrap().to_string(), "A");
275 assert_eq!(
276 PitchClass::try_from("B".to_string()).unwrap().to_string(),
277 "B"
278 );
279 assert_eq!(PitchClass::try_from(3u8).unwrap().to_string(), "3");
280 assert_eq!(PitchClass::try_from(1.5).unwrap().to_string(), "1.5");
281 assert!(PitchClass::try_from("Z").is_err());
282 assert_eq!(PitchClass::try_from(12u8).unwrap().to_string(), "0");
283 assert_eq!(PitchClassSpecifier::from(4.0).to_string(), "4");
284 assert_eq!(PitchClassSpecifier::from("E".to_string()).to_string(), "E");
285 }
286
287 use super::*;
288
289 #[test]
290 fn test_positive() {
291 assert_eq!(convert_pitch_class_to_str(3), "3");
292 assert_eq!(convert_pitch_class_to_str(10), "A");
293 }
294
295 #[test]
296 fn test_wraparound() {
297 assert_eq!(convert_pitch_class_to_str(12), "0");
298 assert_eq!(convert_pitch_class_to_str(13), "1");
299 }
300
301 #[test]
302 fn test_negative() {
303 assert_eq!(convert_pitch_class_to_str(-1), "B");
305 }
306
307 #[test]
308 fn pitch_class_normalizes_numeric_values() {
309 let pitch_class = PitchClass::new(13).unwrap();
310 assert_eq!(pitch_class.number(), 1.0);
311 assert_eq!(pitch_class.integer(), Some(1));
312 assert_eq!(pitch_class.string(), "1");
313
314 let pitch_class = PitchClass::new(-1).unwrap();
315 assert_eq!(pitch_class.number(), 11.0);
316 assert_eq!(pitch_class.string(), "B");
317 }
318
319 #[test]
320 fn pitch_class_accepts_music21_strings() {
321 assert_eq!(PitchClass::new("A").unwrap().number(), 10.0);
322 assert_eq!(PitchClass::new("t").unwrap().number(), 10.0);
323 assert_eq!(PitchClass::new("B").unwrap().number(), 11.0);
324 assert_eq!(PitchClass::new("e").unwrap().number(), 11.0);
325
326 let microtonal = PitchClass::new("10.5").unwrap();
327 assert_eq!(microtonal.number(), 10.5);
328 assert_eq!(microtonal.integer(), None);
329 assert_eq!(microtonal.string(), "10.5");
330 }
331
332 #[test]
333 fn pitch_class_specifier_can_wrap_existing_pitch_class() {
334 let pitch_class = PitchClass::new(14).unwrap();
335 let clone = PitchClass::new(PitchClassSpecifier::from(pitch_class)).unwrap();
336 assert_eq!(clone, pitch_class);
337 }
338
339 #[test]
340 fn pitch_class_supports_rust_conversion_traits() {
341 let parsed: PitchClass = "A".parse().unwrap();
342 assert_eq!(parsed.integer(), Some(10));
343
344 let from_char = PitchClass::try_from('B').unwrap();
345 assert_eq!(from_char.number(), 11.0);
346
347 let from_number = PitchClass::try_from(13).unwrap();
348 assert_eq!(from_number.string(), "1");
349 }
350}