klavier_core/
pitch.rs

1use crate::{solfa::Solfa, key::Key};
2use crate::octave::Octave;
3use crate::sharp_flat::SharpFlat;
4use std::fmt::{self};
5use derive_builder::Builder;
6use super::octave;
7
8/// Error type for pitch-related operations.
9#[derive(Debug)]
10pub enum PitchError {
11    /// The pitch is too low (below MIDI note 0).
12    TooLow(Solfa, Octave, SharpFlat, i32),
13    /// The pitch is too high (above MIDI note 127).
14    TooHigh(Solfa, Octave, SharpFlat, i32),
15    /// The score offset is out of valid range.
16    InvalidScoreOffset(i32),
17}
18
19impl fmt::Display for PitchError {
20    fn fmt(self: &PitchError, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            PitchError::TooLow(solfa, octave, sharp_flat, value) => f.write_fmt(
23                format_args!("Pitch({:?}, {:?}, {:?}) is too low {}", solfa, octave, sharp_flat, value)
24            ),
25            PitchError::TooHigh(solfa, octave, sharp_flat, value) => f.write_fmt(
26                format_args!("Pitch({:?}, {:?}, {:?}) is too high {}", solfa, octave, sharp_flat, value)
27            ),
28            PitchError::InvalidScoreOffset(score_offset) => f.write_fmt(
29                format_args!("Score offset error({})", score_offset)
30            )
31        }
32    }
33}
34
35/// Represents a musical pitch with solfa, octave, and accidental.
36///
37/// A pitch combines a note name (solfa), octave, and accidental (sharp/flat)
38/// to represent a specific musical note. It also caches the MIDI note value
39/// and score offset for efficient access.
40///
41/// # Examples
42///
43/// ```
44/// # use klavier_core::pitch::Pitch;
45/// # use klavier_core::solfa::Solfa;
46/// # use klavier_core::octave::Octave;
47/// # use klavier_core::sharp_flat::SharpFlat;
48/// let middle_c = Pitch::new(Solfa::C, Octave::Oct3, SharpFlat::Null);
49/// assert_eq!(middle_c.value(), 60); // MIDI note 60 (Middle C)
50/// ```
51#[derive(serde::Deserialize, serde::Serialize)]
52#[serde(from = "PitchSerializedForm")]
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Builder)]
54#[builder(default)]
55pub struct Pitch {
56    /// The note name (C, D, E, F, G, A, B).
57    solfa: Solfa,
58    /// The octave number.
59    octave: Octave,
60    /// The accidental (sharp, flat, natural, etc.).
61    sharp_flat: SharpFlat,
62
63    /// Cached MIDI note value (0-127).
64    #[serde(skip)]
65    value: u8,
66    /// Cached score offset for staff position.
67    #[serde(skip)]
68    score_offset: i8,
69}
70
71impl Default for Pitch {
72    fn default() -> Self {
73        DEFAULT
74    }
75}
76
77impl From<PitchSerializedForm> for Pitch {
78    fn from(from: PitchSerializedForm) -> Self {
79        Self::new(from.solfa, from.octave, from.sharp_flat)
80    }
81}
82
83#[derive(serde::Deserialize)]
84struct PitchSerializedForm {
85    solfa: Solfa,
86    octave: Octave,
87    sharp_flat: SharpFlat,
88}
89
90/// Maximum MIDI note value (127).
91pub const MAX_VALUE: i8 = 127;
92/// Minimum MIDI note value (0).
93pub const MIN_VALUE: i8 = 0;
94
95/// Minimum pitch (C at octave -2, MIDI note 0).
96pub const MIN: Pitch = Pitch::new(Solfa::C, Octave::OctM2, SharpFlat::Null);
97/// Maximum pitch (G at octave 8, MIDI note 127).
98pub const MAX: Pitch = Pitch::new(Solfa::G, Octave::Oct8, SharpFlat::Null);
99/// Default pitch (A4, concert A, MIDI note 69).
100const DEFAULT: Pitch = Pitch::new(Solfa::A, Octave::Oct4, SharpFlat::Null);
101
102/// Minimum score offset value.
103pub const MIN_SCORE_OFFSET: i32 = 0;
104/// Maximum score offset value.
105pub const MAX_SCORE_OFFSET: i32 = 74;
106
107impl Pitch {
108    /// Creates a new pitch from solfa, octave, and accidental.
109    ///
110    /// # Arguments
111    ///
112    /// * `solfa` - The note name.
113    /// * `octave` - The octave number.
114    /// * `sharp_flat` - The accidental.
115    ///
116    /// # Panics
117    ///
118    /// Panics if the combination results in an invalid MIDI note (< 0 or > 127).
119    pub const fn new(solfa: Solfa, octave: Octave, sharp_flat: SharpFlat) -> Self {
120        match Self::value_of(solfa, octave, sharp_flat) {
121            Err(_pe) => panic!("Logic error."),
122            Ok(p) => p
123        }
124    }
125
126    /// Applies a key signature to this pitch.
127    ///
128    /// If the pitch has no accidental (Null), this method applies the
129    /// appropriate sharp or flat based on the key signature.
130    ///
131    /// # Arguments
132    ///
133    /// * `key` - The key signature to apply.
134    ///
135    /// # Returns
136    ///
137    /// The pitch with the key signature applied, or an error if invalid.
138    pub fn apply_key(self, key: Key) -> Result<Self, PitchError> {
139        let solfas = Key::SOLFAS;
140        if self.sharp_flat == SharpFlat::Null {
141            match solfas.get(&key) {
142                Some(solfas) =>
143                    if solfas.contains(&self.solfa) {
144                        let sharp_flat = if key.is_flat() { SharpFlat::Flat } else { SharpFlat::Sharp };
145                        Pitch::value_of(self.solfa, self.octave, sharp_flat)
146                    } else {
147                        Ok(self)
148                    }
149                None => Ok(self)
150            }
151        } else {
152            Ok(self)
153        }
154    }
155
156    pub const fn to_value(solfa: Solfa, octave: Octave, sharp_flat: SharpFlat) -> i32 {
157        solfa.pitch_offset() + (octave.value() + Octave::BIAS_VALUE) * 12 + sharp_flat.offset()
158    }
159
160    pub const fn value_of(solfa: Solfa, octave: Octave, sharp_flat: SharpFlat) -> Result<Self, PitchError> {
161        let v: i32 = Self::to_value(solfa, octave, sharp_flat);
162        if v < (MIN_VALUE as i32) {
163            Err(PitchError::TooLow(solfa, octave, sharp_flat, v))
164        } else if (MAX_VALUE as i32) < v {
165            Err(PitchError::TooHigh(solfa, octave, sharp_flat, v))
166        } else {
167            let so = (solfa.score_offset() + 7 * octave.offset()) as i8;
168            Ok(
169                Self {
170                    solfa,
171                    octave,
172                    sharp_flat,
173                    value: v as u8,
174                    score_offset: so
175                }
176            )
177        }
178    }
179
180    pub fn toggle_sharp(self) -> Result<Self, PitchError> {
181        let sharp_flat = match self.sharp_flat {
182            SharpFlat::Sharp => SharpFlat::DoubleSharp,
183            SharpFlat::DoubleSharp => SharpFlat::Null,
184            _ => SharpFlat::Sharp
185        };
186        Self::value_of(self.solfa, self.octave, sharp_flat)
187    }
188
189    pub fn toggle_flat(self) -> Result<Self, PitchError> {
190        let sharp_flat = match self.sharp_flat {
191            SharpFlat::Flat => SharpFlat::DoubleFlat,
192            SharpFlat::DoubleFlat => SharpFlat::Null,
193            _ => SharpFlat::Flat
194        };
195        Self::value_of(self.solfa, self.octave, sharp_flat)
196    }
197
198    pub fn toggle_natural(self) -> Result<Self, PitchError> {
199        let sharp_flat = match self.sharp_flat {
200            SharpFlat::Natural => SharpFlat::Null,
201            _ => SharpFlat::Natural
202        };
203        Self::value_of(self.solfa, self.octave, sharp_flat)
204    }
205
206    pub fn from_score_offset(score_offset: i32) -> Self {
207        let score_offset = score_offset.clamp(MIN_SCORE_OFFSET, MAX_SCORE_OFFSET);
208        let (solfa, octave) = Self::score_offset_to_solfa_octave(score_offset);
209        Self::value_of(solfa, octave, SharpFlat::Null).unwrap()
210    }
211
212    pub fn with_score_offset_delta(self, score_offset_delta: i32) -> Result<Self, PitchError> {
213        let score_offset = self.score_offset as i32 + score_offset_delta;
214        if !(MIN_SCORE_OFFSET..=MAX_SCORE_OFFSET).contains(&score_offset) {
215            return Err(PitchError::InvalidScoreOffset(score_offset));
216        }
217
218        let (solfa, octave) = Self::score_offset_to_solfa_octave(score_offset);
219        Self::value_of(solfa, octave, self.sharp_flat)
220    }
221
222    pub fn score_offset_to_solfa_octave(score_offset: i32) -> (Solfa, Octave) {
223        let octave_offset = score_offset / 7;
224        let solfa_offset = score_offset - (octave_offset * 7);
225        let solfa = Solfa::from_score_offset(solfa_offset);
226        let octave = Octave::from_score_offset(octave_offset).unwrap();
227        (solfa, octave)
228    }
229
230    #[inline]
231    pub const fn score_offset(self) -> i8 { self.score_offset }
232
233    #[inline]
234    pub const fn sharp_flat(self) -> SharpFlat {
235        self.sharp_flat
236    }
237
238    /// Moves the pitch up by one semitone (half step).
239    ///
240    /// # Returns
241    ///
242    /// - `Ok(Pitch)` - The pitch raised by one semitone.
243    /// - `Err(PitchError)` - If already at the maximum pitch.
244    pub fn up(self) -> Result<Self, PitchError> {
245        let mut solfa = self.solfa;
246        let mut octave = self.octave;
247
248        if solfa == Solfa::B {
249            if octave != octave::MAX {
250                octave += 1;
251                solfa = Solfa::C;
252            } else {
253                return Err(PitchError::TooHigh(solfa, octave, self.sharp_flat, -1));
254            }
255        } else {
256            solfa += 1;
257        }
258        Self::value_of(solfa, octave, self.sharp_flat)
259    }
260
261    /// Moves the pitch down by one semitone (half step).
262    ///
263    /// # Returns
264    ///
265    /// - `Ok(Pitch)` - The pitch lowered by one semitone.
266    /// - `Err(PitchError)` - If already at the minimum pitch.
267    pub fn down(self) -> Result<Self, PitchError> {
268        let mut solfa = self.solfa;
269        let mut octave = self.octave;
270
271        if solfa == Solfa::C {
272            if octave != octave::MIN {
273                octave -= 1;
274                solfa = Solfa::B;
275            } else {
276                return Err(PitchError::TooLow(solfa, octave, self.sharp_flat, -1));
277            }
278        } else {
279            solfa -= 1;
280        }
281        Self::value_of(solfa, octave, self.sharp_flat)
282    }
283
284    /// Returns the solfa (note name) of this pitch.
285    #[inline]
286    pub fn solfa(self) -> Solfa {
287        self.solfa
288    }
289
290    /// Returns the octave of this pitch.
291    #[inline]
292    pub fn octave(self) -> Octave {
293        self.octave
294    }
295
296    /// Returns the MIDI note value (0-127) of this pitch.
297    #[inline]
298    pub fn value(self) -> u8 {
299        self.value
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use crate::key::Key;
306    use crate::pitch::MAX_SCORE_OFFSET;
307    use crate::pitch::MIN_SCORE_OFFSET;
308    use crate::pitch::Pitch;
309    use crate::pitch::MIN;
310    use crate::pitch::MAX;
311    use crate::solfa::Solfa;
312    use crate::octave::Octave;
313    use crate::sharp_flat::SharpFlat;
314    use serde_json::Value;
315    use serde_json::json;
316
317    #[test]
318    #[should_panic]
319    fn too_low() {
320        Pitch::new(Solfa::C, Octave::value_of(-2).unwrap(), SharpFlat::Flat);
321    }
322
323    #[test]
324    fn lowest() {
325        assert_eq!(0, MIN.value);
326        assert_eq!(MIN_SCORE_OFFSET, MIN.score_offset() as i32);
327    }
328
329    #[test]
330    fn f7() {
331        assert_eq!(113, Pitch::new(Solfa::F, Octave::Oct7, SharpFlat::Null).value);
332    }
333
334    #[test]
335    fn highest() {
336        assert_eq!(127, MAX.value);
337        assert_eq!(MAX_SCORE_OFFSET, MAX.score_offset() as i32);
338    }
339
340    #[test]
341    #[should_panic]
342    fn too_high() {
343        Pitch::new(Solfa::G, Octave::Oct8, SharpFlat::Sharp);
344    }
345
346    #[test]
347    fn lowest_score_offset() {
348        assert_eq!(0, Pitch::new(Solfa::C, Octave::OctM2, SharpFlat::Null).score_offset());
349    }
350
351    #[test]
352    fn score_offset() {
353        assert_eq!(1, Pitch::new(Solfa::D, Octave::OctM2, SharpFlat::Null).score_offset());
354        assert_eq!(6, Pitch::new(Solfa::B, Octave::OctM2, SharpFlat::Null).score_offset());
355        assert_eq!(7, Pitch::new(Solfa::C, Octave::OctM1, SharpFlat::Null).score_offset());
356    }
357
358    #[test]
359    #[should_panic]
360    fn up_err() {
361        MAX.up().unwrap();
362    } 
363
364    #[test]
365    fn up() {
366        assert_eq!(
367            Pitch::new(Solfa::F, Octave::Oct8, SharpFlat::Null).up().unwrap(),
368            Pitch::new(Solfa::G, Octave::Oct8, SharpFlat::Null)
369        );
370        assert_eq!(
371            Pitch::new(Solfa::B, Octave::Oct7, SharpFlat::Null).up().unwrap(),
372            Pitch::new(Solfa::C, Octave::Oct8, SharpFlat::Null)
373        );
374    }
375
376    #[test]
377    fn down() {
378        assert_eq!(
379            Pitch::new(Solfa::C, Octave::Oct8, SharpFlat::Null).down().unwrap(),
380            Pitch::new(Solfa::B, Octave::Oct7, SharpFlat::Null)
381        );
382        assert_eq!(
383            Pitch::new(Solfa::D, Octave::Oct7, SharpFlat::Null).down().unwrap(),
384            Pitch::new(Solfa::C, Octave::Oct7, SharpFlat::Null)
385        );
386    }
387
388    #[test]
389    #[should_panic]
390    fn down_err() {
391        MIN.down().unwrap();
392    } 
393
394    #[test]
395    fn from_score_offset() {
396        assert_eq!(MIN, Pitch::from_score_offset(MIN_SCORE_OFFSET - 1));
397        assert_eq!(MAX, Pitch::from_score_offset(MAX_SCORE_OFFSET + 1));
398
399        assert_eq!(MIN, Pitch::from_score_offset(MIN_SCORE_OFFSET));
400        assert_eq!(MAX, Pitch::from_score_offset(MAX_SCORE_OFFSET));
401    }
402
403    #[test]
404    fn with_score_offset_delta() {
405        assert_eq!(
406            Pitch::new(Solfa::C, Octave::Oct8, SharpFlat::Flat).with_score_offset_delta(1).unwrap(),
407            Pitch::new(Solfa::D, Octave::Oct8, SharpFlat::Flat)
408        );
409
410        assert_eq!(
411            Pitch::new(Solfa::C, Octave::Oct7, SharpFlat::Sharp).with_score_offset_delta(8).unwrap(),
412            Pitch::new(Solfa::D, Octave::Oct8, SharpFlat::Sharp)
413        );
414
415        assert_eq!(
416            Pitch::new(Solfa::C, Octave::Oct7, SharpFlat::Sharp).with_score_offset_delta(-8).unwrap(),
417            Pitch::new(Solfa::B, Octave::Oct5, SharpFlat::Sharp)
418        );
419    }
420
421    #[test]
422    #[should_panic]
423    fn with_score_offset_delta_min_error() {
424        let _ = MIN.with_score_offset_delta(-1).unwrap();
425    }
426
427    #[test]
428    #[should_panic]
429    fn with_score_offset_delta_max_error() {
430        let _ = MAX.with_score_offset_delta(1).unwrap();
431    }
432
433    #[test]
434    fn can_serialize_pitch() {
435        let json_str = serde_json::to_string(&Pitch::new(
436            Solfa::C, Octave::Oct2, SharpFlat::DoubleFlat
437        )).unwrap();
438        let json: Value = serde_json::from_str(&json_str).unwrap();
439        assert_eq!(
440            json,
441            json!({
442                "octave": "Oct2",
443                "sharp_flat": "DoubleFlat",
444                "solfa": "C"
445            })
446        );
447    }
448
449    #[test]
450    fn can_deserialize_pitch() {
451        let pitch: Pitch = serde_json::from_str(r#"
452            {
453                "octave": "Oct2",
454                "sharp_flat": "DoubleFlat",
455                "solfa": "C"
456            }"#).unwrap();
457        let expected = Pitch::new(Solfa::C, Octave::Oct2, SharpFlat::DoubleFlat);
458        assert_eq!(pitch, expected);
459        assert_eq!(pitch.score_offset, expected.score_offset);
460    }
461
462    #[test]
463    fn apply() {
464        let pitch = Pitch::new(Solfa::F, Octave::Oct1, SharpFlat::Null);
465        assert_eq!(pitch.apply_key(Key::SHARP_1).unwrap(), Pitch::new(Solfa::F, Octave::Oct1, SharpFlat::Sharp));
466
467        let pitch = Pitch::new(Solfa::F, Octave::Oct1, SharpFlat::Flat);
468        assert_eq!(pitch.apply_key(Key::SHARP_1).unwrap(), Pitch::new(Solfa::F, Octave::Oct1, SharpFlat::Flat));
469
470        let pitch = Pitch::new(Solfa::E, Octave::Oct1, SharpFlat::Null);
471        assert_eq!(pitch.apply_key(Key::FLAT_2).unwrap(), Pitch::new(Solfa::E, Octave::Oct1, SharpFlat::Flat));
472
473        let pitch = Pitch::new(Solfa::E, Octave::Oct1, SharpFlat::Sharp);
474        assert_eq!(pitch.apply_key(Key::FLAT_2).unwrap(), Pitch::new(Solfa::E, Octave::Oct1, SharpFlat::Sharp));
475
476        let pitch = Pitch::new(Solfa::F, Octave::Oct1, SharpFlat::Null);
477        assert_eq!(pitch.apply_key(Key::FLAT_2).unwrap(), Pitch::new(Solfa::F, Octave::Oct1, SharpFlat::Null));
478    }
479}