sim_lib_pitch_set/
model.rs1use thiserror::Error;
2
3use sim_lib_pitch_core::{Pitch, PitchClass};
4
5#[derive(Debug, Error, Clone, PartialEq, Eq)]
7pub enum PitchSetError {
8 #[error("invalid MIDI key {0}")]
10 InvalidMidiKey(u8),
11 #[error("invalid pitch-class mask {0}")]
13 InvalidPitchClassMask(u16),
14 #[error("invalid third stack encoding")]
16 InvalidThirdStackEncoding,
17 #[error("invalid third stack signature")]
19 InvalidThirdStack,
20}
21
22#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
37pub struct PitchClassMask(u16);
38
39impl PitchClassMask {
40 const VALID_BITS: u16 = 0x0fff;
41
42 pub fn new(bits: u16) -> Result<Self, PitchSetError> {
44 if bits & !Self::VALID_BITS == 0 {
45 Ok(Self(bits))
46 } else {
47 Err(PitchSetError::InvalidPitchClassMask(bits))
48 }
49 }
50
51 pub const fn bits(self) -> u16 {
53 self.0
54 }
55
56 pub fn from_pitch_classes(pitch_classes: &[PitchClass]) -> Self {
58 let mut bits = 0u16;
59 for pitch_class in pitch_classes {
60 bits |= 1u16 << pitch_class.value();
61 }
62 Self(bits)
63 }
64
65 pub fn pitch_classes(self) -> Vec<PitchClass> {
67 (0..12)
68 .filter(|bit| self.0 & (1u16 << bit) != 0)
69 .map(|bit| PitchClass::new(bit).expect("mask iteration yields valid pitch classes"))
70 .collect()
71 }
72
73 pub fn rotate(self, semitones: i32) -> Self {
75 let shift = semitones.rem_euclid(12) as u32;
76 let bits = self.0;
77 Self(((bits << shift) | (bits >> (12 - shift))) & Self::VALID_BITS)
78 }
79
80 pub fn invert(self, axis: PitchClass) -> Self {
82 let mut out = 0u16;
83 for pitch_class in self.pitch_classes() {
84 out |= 1u16 << pitch_class.invert(axis).value();
85 }
86 Self(out)
87 }
88
89 pub fn normalize(self) -> Self {
92 (0..12)
93 .map(|shift| self.rotate(-shift))
94 .min_by_key(|mask| mask.bits())
95 .unwrap_or(self)
96 }
97
98 pub fn count_bits(self) -> u32 {
100 self.0.count_ones()
101 }
102
103 pub fn interval_vector(self) -> IntervalVector {
106 let pitch_classes = self.pitch_classes();
107 let mut bins = [0u16; 6];
108 for (index, a) in pitch_classes.iter().enumerate() {
109 for b in pitch_classes.iter().skip(index + 1) {
110 let class = a.interval_class(*b);
111 if class > 0 {
112 bins[(class - 1) as usize] += 1;
113 }
114 }
115 }
116 IntervalVector(bins)
117 }
118}
119
120#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
125pub struct PitchRangeMask {
126 pub bits: u128,
128}
129
130impl PitchRangeMask {
131 pub fn set(&mut self, midi_key: u8) {
133 self.bits |= 1u128 << midi_key;
134 }
135
136 pub fn clear(&mut self, midi_key: u8) {
138 self.bits &= !(1u128 << midi_key);
139 }
140
141 pub fn contains(self, midi_key: u8) -> bool {
143 self.bits & (1u128 << midi_key) != 0
144 }
145
146 pub fn union(self, other: Self) -> Self {
148 Self {
149 bits: self.bits | other.bits,
150 }
151 }
152
153 pub fn intersection(self, other: Self) -> Self {
155 Self {
156 bits: self.bits & other.bits,
157 }
158 }
159
160 pub fn difference(self, other: Self) -> Self {
162 Self {
163 bits: self.bits & !other.bits,
164 }
165 }
166
167 pub fn to_pitches(self) -> Vec<Pitch> {
169 (0..128u8)
170 .filter(|key| self.contains(*key))
171 .map(Pitch::from_midi)
172 .collect()
173 }
174}
175
176#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
179pub struct IntervalVector(pub [u16; 6]);
180
181#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
186pub struct BitChord {
187 pub mask: PitchClassMask,
189 pub root: Option<PitchClass>,
191}
192
193impl BitChord {
194 pub fn canonical(self) -> Self {
197 if self.root.is_some() {
198 self
199 } else {
200 Self {
201 mask: self.mask.normalize(),
202 root: None,
203 }
204 }
205 }
206}
207
208#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
211pub enum ThirdStep {
212 Minor,
214 Major,
216}
217
218#[derive(Clone, Debug, PartialEq, Eq, Hash)]
224pub struct ThirdStackSignature {
225 pub root: PitchClass,
227 pub steps: Vec<ThirdStep>,
229 pub guard: bool,
231}
232
233impl ThirdStackSignature {
234 pub fn validate(&self) -> Result<(), PitchSetError> {
237 let mut minor_run = 0usize;
238 let mut major_run = 0usize;
239 for step in &self.steps {
240 match step {
241 ThirdStep::Minor => {
242 minor_run += 1;
243 major_run = 0;
244 }
245 ThirdStep::Major => {
246 major_run += 1;
247 minor_run = 0;
248 }
249 }
250 if minor_run >= 4 || major_run >= 3 {
251 return Err(PitchSetError::InvalidThirdStack);
252 }
253 }
254 Ok(())
255 }
256
257 pub fn encode(&self) -> Result<u32, PitchSetError> {
259 self.validate()?;
260 let mut encoded = u32::from(self.root.value());
261 for (index, step) in self.steps.iter().enumerate() {
262 let bit = if matches!(step, ThirdStep::Major) {
263 1u32
264 } else {
265 0u32
266 };
267 encoded |= bit << (4 + index);
268 }
269 if self.guard {
270 encoded |= 1u32 << (4 + self.steps.len());
271 }
272 Ok(encoded)
273 }
274
275 pub fn decode(encoded: u32) -> Result<Self, PitchSetError> {
278 let root =
279 PitchClass::new(u8::try_from(encoded & 0x0f).expect("third-stack root nibble fits u8"))
280 .map_err(|_| PitchSetError::InvalidThirdStackEncoding)?;
281 let mut steps = Vec::new();
282 let mut index = 4u32;
283 let mut guard = false;
284 while index < 31 {
285 let bit = (encoded >> index) & 1;
286 if ((encoded >> (index + 1)) & 1) == 0 && bit == 1 && index > 4 {
287 guard = true;
288 break;
289 }
290 steps.push(if bit == 0 {
291 ThirdStep::Minor
292 } else {
293 ThirdStep::Major
294 });
295 index += 1;
296 if steps.len() >= 8 {
297 break;
298 }
299 }
300 let signature = Self { root, steps, guard };
301 signature.validate()?;
302 Ok(signature)
303 }
304
305 pub fn family_tag(&self) -> char {
308 let majors = self
309 .steps
310 .iter()
311 .filter(|step| matches!(step, ThirdStep::Major))
312 .count();
313 match majors {
314 0..=2 => 'w',
315 3 => 'x',
316 4 => 'y',
317 _ => 'z',
318 }
319 }
320
321 pub fn to_mask(&self) -> PitchClassMask {
324 let mut pitch_classes = vec![self.root];
325 let mut current = self.root;
326 for step in &self.steps {
327 current = current.transpose(match step {
328 ThirdStep::Minor => 3,
329 ThirdStep::Major => 4,
330 });
331 pitch_classes.push(current);
332 }
333 PitchClassMask::from_pitch_classes(&pitch_classes)
334 }
335}