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}