music21_rs/pitch/
pitchclass.rs1use crate::{
2 defaults::{FloatType, IntegerType, PITCH_SPACE_SIGNIFICANT_DIGITS},
3 error::{Error, Result},
4};
5
6use super::pitchclassstring::PitchClassString;
7use std::fmt::{Display, Formatter};
8use std::str::FromStr;
9
10#[derive(Clone, Debug, PartialEq)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub enum PitchClassSpecifier {
14 Number(FloatType),
16 String(String),
18 PitchClass(PitchClass),
20}
21
22impl PitchClassSpecifier {
23 pub(crate) fn to_number(&self) -> Result<FloatType> {
24 match self {
25 Self::Number(value) => Ok(*value),
26 Self::String(value) => parse_pitch_class_string(value),
27 Self::PitchClass(pitch_class) => Ok(pitch_class.number()),
28 }
29 }
30}
31
32impl From<IntegerType> for PitchClassSpecifier {
33 fn from(value: IntegerType) -> Self {
34 Self::Number(value as FloatType)
35 }
36}
37
38impl From<u8> for PitchClassSpecifier {
39 fn from(value: u8) -> Self {
40 Self::Number(value as FloatType)
41 }
42}
43
44impl From<FloatType> for PitchClassSpecifier {
45 fn from(value: FloatType) -> Self {
46 Self::Number(value)
47 }
48}
49
50impl From<char> for PitchClassSpecifier {
51 fn from(value: char) -> Self {
52 Self::String(value.to_string())
53 }
54}
55
56impl From<&str> for PitchClassSpecifier {
57 fn from(value: &str) -> Self {
58 Self::String(value.to_string())
59 }
60}
61
62impl From<String> for PitchClassSpecifier {
63 fn from(value: String) -> Self {
64 Self::String(value)
65 }
66}
67
68impl From<PitchClass> for PitchClassSpecifier {
69 fn from(value: PitchClass) -> Self {
70 Self::PitchClass(value)
71 }
72}
73
74impl Display for PitchClassSpecifier {
75 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
76 match self {
77 Self::Number(number) => write!(f, "{number}"),
78 Self::String(value) => write!(f, "{value}"),
79 Self::PitchClass(pitch_class) => write!(f, "{pitch_class}"),
80 }
81 }
82}
83
84impl FromStr for PitchClass {
85 type Err = Error;
86
87 fn from_str(value: &str) -> Result<Self> {
88 Self::new(value)
89 }
90}
91
92impl TryFrom<&str> for PitchClass {
93 type Error = Error;
94
95 fn try_from(value: &str) -> Result<Self> {
96 Self::new(value)
97 }
98}
99
100impl TryFrom<String> for PitchClass {
101 type Error = Error;
102
103 fn try_from(value: String) -> Result<Self> {
104 Self::new(value)
105 }
106}
107
108impl TryFrom<char> for PitchClass {
109 type Error = Error;
110
111 fn try_from(value: char) -> Result<Self> {
112 Self::new(value)
113 }
114}
115
116impl TryFrom<IntegerType> for PitchClass {
117 type Error = Error;
118
119 fn try_from(value: IntegerType) -> Result<Self> {
120 Self::new(value)
121 }
122}
123
124impl TryFrom<u8> for PitchClass {
125 type Error = Error;
126
127 fn try_from(value: u8) -> Result<Self> {
128 Self::new(value)
129 }
130}
131
132impl TryFrom<FloatType> for PitchClass {
133 type Error = Error;
134
135 fn try_from(value: FloatType) -> Result<Self> {
136 Self::new(value)
137 }
138}
139
140#[derive(Clone, Copy, Debug, PartialEq)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
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(crate) fn convert_pitch_class_to_str(pc: IntegerType) -> String {
200 let pc = pc.rem_euclid(12);
202 format!("{pc:X}")
203}
204
205fn pitch_class_to_string(pc: FloatType) -> String {
206 let pc = normalize_pitch_class(pc);
207 if pc.fract() == 0.0 {
208 return convert_pitch_class_to_str(pc as IntegerType);
209 }
210
211 trim_float(pc)
212}
213
214fn normalize_pitch_class(pc: FloatType) -> FloatType {
215 let factor = (10 as FloatType).powi(PITCH_SPACE_SIGNIFICANT_DIGITS as IntegerType);
216 let normalized = (pc.rem_euclid(12.0) * factor).round() / factor;
217 if normalized == 12.0 { 0.0 } else { normalized }
218}
219
220fn parse_pitch_class_string(value: &str) -> Result<FloatType> {
221 let value = value.trim();
222 if value.chars().count() == 1
223 && let Ok(pc_string) = PitchClassString::try_from(value.chars().next().unwrap())
224 {
225 return Ok(pc_string.to_number() as FloatType);
226 }
227
228 value
229 .parse::<FloatType>()
230 .map_err(|err| Error::PitchClass(format!("cannot parse pitch class {value:?}: {err}")))
231}
232
233fn trim_float(value: FloatType) -> String {
234 let text = format!("{value:.6}");
235 text.trim_end_matches('0').trim_end_matches('.').to_string()
236}
237
238pub(crate) fn convert_ps_to_oct(ps: FloatType) -> IntegerType {
239 let factor = (10 as FloatType).powi(PITCH_SPACE_SIGNIFICANT_DIGITS as IntegerType);
240 let ps_rounded = (ps * factor).round() / factor;
241 (ps_rounded / 12.0).floor() as IntegerType - 1
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn test_positive() {
250 assert_eq!(convert_pitch_class_to_str(3), "3");
251 assert_eq!(convert_pitch_class_to_str(10), "A");
252 }
253
254 #[test]
255 fn test_wraparound() {
256 assert_eq!(convert_pitch_class_to_str(12), "0");
257 assert_eq!(convert_pitch_class_to_str(13), "1");
258 }
259
260 #[test]
261 fn test_negative() {
262 assert_eq!(convert_pitch_class_to_str(-1), "B");
264 }
265
266 #[test]
267 fn pitch_class_normalizes_numeric_values() {
268 let pitch_class = PitchClass::new(13).unwrap();
269 assert_eq!(pitch_class.number(), 1.0);
270 assert_eq!(pitch_class.integer(), Some(1));
271 assert_eq!(pitch_class.string(), "1");
272
273 let pitch_class = PitchClass::new(-1).unwrap();
274 assert_eq!(pitch_class.number(), 11.0);
275 assert_eq!(pitch_class.string(), "B");
276 }
277
278 #[test]
279 fn pitch_class_accepts_music21_strings() {
280 assert_eq!(PitchClass::new("A").unwrap().number(), 10.0);
281 assert_eq!(PitchClass::new("t").unwrap().number(), 10.0);
282 assert_eq!(PitchClass::new("B").unwrap().number(), 11.0);
283 assert_eq!(PitchClass::new("e").unwrap().number(), 11.0);
284
285 let microtonal = PitchClass::new("10.5").unwrap();
286 assert_eq!(microtonal.number(), 10.5);
287 assert_eq!(microtonal.integer(), None);
288 assert_eq!(microtonal.string(), "10.5");
289 }
290
291 #[test]
292 fn pitch_class_specifier_can_wrap_existing_pitch_class() {
293 let pitch_class = PitchClass::new(14).unwrap();
294 let clone = PitchClass::new(PitchClassSpecifier::from(pitch_class)).unwrap();
295 assert_eq!(clone, pitch_class);
296 }
297
298 #[test]
299 fn pitch_class_supports_rust_conversion_traits() {
300 let parsed: PitchClass = "A".parse().unwrap();
301 assert_eq!(parsed.integer(), Some(10));
302
303 let from_char = PitchClass::try_from('B').unwrap();
304 assert_eq!(from_char.number(), 11.0);
305
306 let from_number = PitchClass::try_from(13).unwrap();
307 assert_eq!(from_number.string(), "1");
308 }
309}