redact_composer_musical/
pitch_class.rs

1use crate::{Interval, Note, NoteIter, NoteIterator, NoteName, PitchClassCollection};
2use std::ops::{Add, AddAssign, RangeBounds, Sub, SubAssign};
3
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6
7#[cfg(feature = "redact-composer")]
8use redact_composer_core::derive::Element;
9
10/// An octave-independent note.
11#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13#[cfg_attr(feature = "redact-composer", derive(Element))]
14pub struct PitchClass(pub u8);
15
16impl PitchClass {
17    /// Returns all valid [`PitchClass`] values (0..=11).
18    pub fn values() -> Vec<PitchClass> {
19        (0..12).map(PitchClass::from).collect()
20    }
21
22    /// Returns all pitch classes contained in the given note range.
23    /// ```
24    /// use redact_composer_musical::{Note, NoteName::{C, F}, PitchClass as PC};
25    ///
26    /// assert_eq!(
27    ///     PC::all_in_range(Note::from((C, 3))..=Note::from((F, 3))),
28    ///     vec![PC(0), PC(1), PC(2), PC(3), PC(4), PC(5)]
29    /// );
30    ///
31    /// assert_eq!(
32    ///     PC::all_in_range(Note::from((C, 3))..=Note::from((C, 5))),
33    ///     PC::values()
34    /// );
35    /// ```
36    pub fn all_in_range<R: RangeBounds<Note>>(range: R) -> Vec<PitchClass> {
37        NoteIter::chromatic(range)
38            .take(12)
39            .map(|note| note.pitch_class())
40            .collect()
41    }
42
43    /// Returns the [`Note`] with this pitch class in a given octave.
44    /// ```
45    /// use redact_composer_musical::{Note, NoteName::C, PitchClass};
46    ///
47    /// assert_eq!(PitchClass::from(C).in_octave(4), Note(60));
48    /// ```
49    pub fn in_octave(&self, octave: i8) -> Note {
50        (*self, octave).into()
51    }
52
53    /// Returns the next [`Note`] of this pitch class above the given note. If the given note is already of this pitch
54    /// class, the note an octave above is returned.
55    /// ```
56    /// use redact_composer_musical::{Note, NoteName::{C, G}, PitchClass};
57    ///
58    /// assert_eq!(PitchClass::from(G).above(&Note::from((C, 3))), Note::from((G, 3)));
59    /// assert_eq!(PitchClass::from(G).above(&Note::from((G, 3))), Note::from((G, 4)));
60    /// ```
61    pub fn above(&self, note: &Note) -> Note {
62        let pitch_in_same_octave = Note::from((*self, note.octave()));
63        if pitch_in_same_octave > *note {
64            pitch_in_same_octave
65        } else {
66            pitch_in_same_octave + Interval::Octave
67        }
68    }
69
70    /// Returns the next [`Note`] of this pitch class at or above the given note.
71    /// ```
72    /// use redact_composer_musical::{Note, NoteName::{C, G}, PitchClass};
73    ///
74    /// assert_eq!(PitchClass::from(G).at_or_above(&Note::from((C, 3))), Note::from((G, 3)));
75    /// assert_eq!(PitchClass::from(G).at_or_above(&Note::from((G, 3))), Note::from((G, 3)));
76    /// ```
77    pub fn at_or_above(&self, note: &Note) -> Note {
78        if *note == *self {
79            *note
80        } else {
81            self.above(note)
82        }
83    }
84
85    /// Returns the next [`Note`] of this pitch class below the given note. If the given note is already of this pitch
86    /// class, the note an octave below is returned.
87    /// ```
88    /// use redact_composer_musical::{Note, NoteName::{C, G}, PitchClass};
89    ///
90    /// assert_eq!(PitchClass::from(G).below(&Note::from((C, 3))), Note::from((G, 2)));
91    /// assert_eq!(PitchClass::from(G).below(&Note::from((G, 3))), Note::from((G, 2)));
92    /// ```
93    pub fn below(&self, note: &Note) -> Note {
94        let pitch_in_same_octave = Note::from((*self, note.octave()));
95        if pitch_in_same_octave < *note {
96            pitch_in_same_octave
97        } else {
98            pitch_in_same_octave - Interval::Octave
99        }
100    }
101
102    /// Returns the next [`Note`] of this pitch class at or below the given note.
103    /// ```
104    /// use redact_composer_musical::{Note, NoteName::{C, G}, PitchClass};
105    ///
106    /// assert_eq!(PitchClass::from(G).at_or_below(&Note::from((C, 3))), Note::from((G, 2)));
107    /// assert_eq!(PitchClass::from(G).at_or_below(&Note::from((G, 3))), Note::from((G, 3)));
108    /// ```
109    pub fn at_or_below(&self, note: &Note) -> Note {
110        if *note == *self {
111            *note
112        } else {
113            self.below(note)
114        }
115    }
116
117    /// Returns the simple interval (ascending) from this pitch class to the nearest `other` pitch class.
118    /// ```
119    /// use redact_composer_musical::{Interval, NoteName::{C, G}, PitchClass};
120    ///
121    /// assert_eq!(PitchClass::from(C).interval_to(&G.into()), Interval::P5);
122    /// ```
123    pub fn interval_to(&self, other: &PitchClass) -> Interval {
124        let (first, second) = if self.0 <= other.0 {
125            (self.0, other.0)
126        } else {
127            (self.0, other.0 + 12)
128        };
129
130        Interval(second - first)
131    }
132
133    /// Returns the simple interval (ascending) from some `other` pitch class to this one.
134    /// ```
135    /// use redact_composer_musical::{Interval, NoteName::{C, G}, PitchClass};
136    ///
137    /// assert_eq!(PitchClass::from(C).interval_from(&G.into()), Interval::P4);
138    /// ```
139    pub fn interval_from(&self, other: &PitchClass) -> Interval {
140        self.interval_to(other).inversion()
141    }
142}
143
144impl NoteIterator for PitchClass {
145    fn iter_notes_in_range<R: RangeBounds<Note>>(&self, note_range: R) -> NoteIter<R> {
146        NoteIter::from((*self, vec![Interval::P1], note_range))
147    }
148}
149
150impl PitchClassCollection for PitchClass {
151    fn pitch_classes(&self) -> Vec<PitchClass> {
152        vec![*self]
153    }
154}
155
156impl From<NoteName> for PitchClass {
157    fn from(value: NoteName) -> Self {
158        PitchClass(value.into())
159    }
160}
161
162impl From<Note> for PitchClass {
163    fn from(value: Note) -> Self {
164        value.pitch_class()
165    }
166}
167
168impl From<u8> for PitchClass {
169    fn from(value: u8) -> Self {
170        Self(value % 12)
171    }
172}
173
174impl PartialEq<NoteName> for PitchClass {
175    /// ```
176    /// use redact_composer_musical::{NoteName::C, PitchClass};
177    /// assert!(PitchClass(0).eq(&C));
178    /// ```
179    fn eq(&self, note_name: &NoteName) -> bool {
180        *self == PitchClass::from(*note_name)
181    }
182}
183
184impl Add<Interval> for PitchClass {
185    type Output = PitchClass;
186
187    /// Returns the pitch class a given interval above this.
188    /// ```
189    /// use redact_composer_musical::{Interval as I, NoteName::{C, G}, PitchClass};
190    ///
191    /// assert_eq!(PitchClass::from(C) + I::P5, PitchClass::from(G));
192    /// assert_eq!(PitchClass::from(C) + I::Octave, PitchClass::from(C));
193    /// ```
194    fn add(self, rhs: Interval) -> Self::Output {
195        let mut output = self;
196        output += rhs;
197        output
198    }
199}
200
201impl AddAssign<Interval> for PitchClass {
202    fn add_assign(&mut self, rhs: Interval) {
203        self.0 = (self.0 + rhs.to_simple().0) % 12;
204    }
205}
206
207impl Sub<Interval> for PitchClass {
208    type Output = PitchClass;
209
210    /// Returns the pitch class a given interval below this.
211    /// ```
212    /// use redact_composer_musical::{Interval as I, NoteName::{C, F}, PitchClass};
213    ///
214    /// assert_eq!(PitchClass::from(C) - I::P5, PitchClass::from(F));
215    /// assert_eq!(PitchClass::from(C) - I::Octave, PitchClass::from(C));
216    /// ```
217    fn sub(self, rhs: Interval) -> Self::Output {
218        let mut output = self;
219        output -= rhs;
220        output
221    }
222}
223
224impl SubAssign<Interval> for PitchClass {
225    fn sub_assign(&mut self, rhs: Interval) {
226        self.0 = (self.0 + 12 - rhs.to_simple().0) % 12;
227    }
228}