1use thiserror::Error;
2
3use sim_lib_pitch_core::{Pitch, PitchClass};
4
5use crate::{SetClass, SetEquivalence, classify_set, conventional};
6
7#[derive(Debug, Error, Clone, PartialEq, Eq)]
9pub enum PitchSetError {
10 #[error("invalid MIDI key {0}")]
12 InvalidMidiKey(u8),
13 #[error("invalid pitch-class mask {0}")]
15 InvalidPitchClassMask(u16),
16 #[error("invalid third stack encoding")]
18 InvalidThirdStackEncoding,
19 #[error("invalid third stack signature")]
21 InvalidThirdStack,
22}
23
24#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
39pub struct PitchClassMask(u16);
40
41impl PitchClassMask {
42 const VALID_BITS: u16 = 0x0fff;
43
44 pub fn new(bits: u16) -> Result<Self, PitchSetError> {
46 if bits & !Self::VALID_BITS == 0 {
47 Ok(Self(bits))
48 } else {
49 Err(PitchSetError::InvalidPitchClassMask(bits))
50 }
51 }
52
53 pub const fn bits(self) -> u16 {
55 self.0
56 }
57
58 pub fn from_pitch_classes(pitch_classes: &[PitchClass]) -> Self {
60 let mut bits = 0u16;
61 for pitch_class in pitch_classes {
62 bits |= 1u16 << pitch_class.value();
63 }
64 Self(bits)
65 }
66
67 pub fn pitch_classes(self) -> Vec<PitchClass> {
69 (0..12)
70 .filter(|bit| self.0 & (1u16 << bit) != 0)
71 .map(|bit| PitchClass::new(bit).expect("mask iteration yields valid pitch classes"))
72 .collect()
73 }
74
75 pub fn rotate(self, semitones: i32) -> Self {
77 let shift = semitones.rem_euclid(12) as u32;
78 let bits = self.0;
79 Self(((bits << shift) | (bits >> (12 - shift))) & Self::VALID_BITS)
80 }
81
82 pub fn invert(self, axis: PitchClass) -> Self {
84 let mut out = 0u16;
85 for pitch_class in self.pitch_classes() {
86 out |= 1u16 << pitch_class.invert(axis).value();
87 }
88 Self(out)
89 }
90
91 pub fn invert_tni(self, index: u8) -> Self {
98 let index = i32::from(index % 12);
99 let classes: Vec<_> = self
100 .pitch_classes()
101 .into_iter()
102 .map(|pitch_class| {
103 PitchClass::new((index - i32::from(pitch_class.value())).rem_euclid(12) as u8)
104 .expect("TnI folds to a valid pitch class")
105 })
106 .collect();
107 Self::from_pitch_classes(&classes)
108 }
109
110 pub fn normalize(self) -> Self {
113 (0..12)
114 .map(|shift| self.rotate(-shift))
115 .min_by_key(|mask| mask.bits())
116 .unwrap_or(self)
117 }
118
119 pub fn normal_order(self) -> Vec<PitchClass> {
125 conventional::normal_order(self.pitch_classes())
126 }
127
128 pub fn classify(self, equivalence: SetEquivalence) -> SetClass {
130 classify_set(self, equivalence)
131 }
132
133 pub fn is_subset_of(self, other: Self) -> bool {
135 self.0 & !other.0 == 0
136 }
137
138 pub fn is_superset_of(self, other: Self) -> bool {
140 other.is_subset_of(self)
141 }
142
143 pub fn complement(self) -> Self {
145 Self(!self.0 & Self::VALID_BITS)
146 }
147
148 pub fn union(self, other: Self) -> Self {
150 Self(self.0 | other.0)
151 }
152
153 pub fn intersection(self, other: Self) -> Self {
155 Self(self.0 & other.0)
156 }
157
158 pub fn difference(self, other: Self) -> Self {
160 Self(self.0 & !other.0)
161 }
162
163 pub fn symmetric_difference(self, other: Self) -> Self {
165 Self(self.0 ^ other.0)
166 }
167
168 pub fn is_disjoint_from(self, other: Self) -> bool {
170 self.intersection(other).bits() == 0
171 }
172
173 pub fn transpositional_symmetries(self) -> Vec<u8> {
175 (0..12)
176 .filter(|shift| self.rotate(i32::from(*shift)) == self)
177 .collect()
178 }
179
180 pub fn inversional_symmetries(self) -> Vec<PitchClass> {
182 (0..12)
183 .filter_map(|axis| {
184 let pitch_class =
185 PitchClass::new(axis).expect("symmetry axis iteration yields pitch classes");
186 (self.invert(pitch_class) == self).then_some(pitch_class)
187 })
188 .collect()
189 }
190
191 pub fn roots(self) -> Vec<PitchClass> {
193 let mut roots = Vec::new();
194 for root in self.pitch_classes() {
195 let contains = |semitones| self.0 & (1u16 << root.transpose(semitones).value()) != 0;
196 if contains(7) && (contains(3) || contains(4)) {
197 roots.push(root);
198 }
199 }
200 roots
201 }
202
203 pub fn is_z_related_to(self, other: Self) -> bool {
206 self.count_bits() == other.count_bits()
207 && self.interval_vector() == other.interval_vector()
208 && classify_set(self, SetEquivalence::TranspositionInversion).prime
209 != classify_set(other, SetEquivalence::TranspositionInversion).prime
210 }
211
212 pub fn count_bits(self) -> u32 {
214 self.0.count_ones()
215 }
216
217 pub fn interval_vector(self) -> IntervalVector {
220 let pitch_classes = self.pitch_classes();
221 let mut bins = [0u16; 6];
222 for (index, a) in pitch_classes.iter().enumerate() {
223 for b in pitch_classes.iter().skip(index + 1) {
224 let class = a.interval_class(*b);
225 if class > 0 {
226 bins[(class - 1) as usize] += 1;
227 }
228 }
229 }
230 IntervalVector(bins)
231 }
232}
233
234#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
239pub struct PitchRangeMask {
240 pub bits: u128,
242}
243
244impl PitchRangeMask {
245 pub fn set(&mut self, midi_key: u8) {
247 self.bits |= 1u128 << midi_key;
248 }
249
250 pub fn clear(&mut self, midi_key: u8) {
252 self.bits &= !(1u128 << midi_key);
253 }
254
255 pub fn contains(self, midi_key: u8) -> bool {
257 self.bits & (1u128 << midi_key) != 0
258 }
259
260 pub fn union(self, other: Self) -> Self {
262 Self {
263 bits: self.bits | other.bits,
264 }
265 }
266
267 pub fn intersection(self, other: Self) -> Self {
269 Self {
270 bits: self.bits & other.bits,
271 }
272 }
273
274 pub fn difference(self, other: Self) -> Self {
276 Self {
277 bits: self.bits & !other.bits,
278 }
279 }
280
281 pub fn to_pitches(self) -> Vec<Pitch> {
283 (0..128u8)
284 .filter(|key| self.contains(*key))
285 .map(Pitch::from_midi)
286 .collect()
287 }
288}
289
290#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
293pub struct IntervalVector(pub [u16; 6]);
294
295#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
300pub struct BitChord {
301 pub mask: PitchClassMask,
303 pub root: Option<PitchClass>,
305}
306
307impl BitChord {
308 pub fn canonical(self) -> Self {
311 if self.root.is_some() {
312 self
313 } else {
314 Self {
315 mask: self.mask.normalize(),
316 root: None,
317 }
318 }
319 }
320}
321
322#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
325pub enum ThirdStep {
326 Minor,
328 Major,
330}
331
332#[derive(Clone, Debug, PartialEq, Eq, Hash)]
338pub struct ThirdStackSignature {
339 pub root: PitchClass,
341 pub steps: Vec<ThirdStep>,
343 pub guard: bool,
345}
346
347impl ThirdStackSignature {
348 pub fn validate(&self) -> Result<(), PitchSetError> {
351 let mut minor_run = 0usize;
352 let mut major_run = 0usize;
353 for step in &self.steps {
354 match step {
355 ThirdStep::Minor => {
356 minor_run += 1;
357 major_run = 0;
358 }
359 ThirdStep::Major => {
360 major_run += 1;
361 minor_run = 0;
362 }
363 }
364 if minor_run >= 4 || major_run >= 3 {
365 return Err(PitchSetError::InvalidThirdStack);
366 }
367 }
368 Ok(())
369 }
370
371 pub fn encode(&self) -> Result<u32, PitchSetError> {
373 self.validate()?;
374 let mut encoded = u32::from(self.root.value());
375 for (index, step) in self.steps.iter().enumerate() {
376 let bit = if matches!(step, ThirdStep::Major) {
377 1u32
378 } else {
379 0u32
380 };
381 encoded |= bit << (4 + index);
382 }
383 if self.guard {
384 encoded |= 1u32 << (4 + self.steps.len());
385 }
386 Ok(encoded)
387 }
388
389 pub fn decode(encoded: u32) -> Result<Self, PitchSetError> {
392 let root =
393 PitchClass::new(u8::try_from(encoded & 0x0f).expect("third-stack root nibble fits u8"))
394 .map_err(|_| PitchSetError::InvalidThirdStackEncoding)?;
395 let mut steps = Vec::new();
396 let mut index = 4u32;
397 let mut guard = false;
398 while index < 31 {
399 let bit = (encoded >> index) & 1;
400 if ((encoded >> (index + 1)) & 1) == 0 && bit == 1 && index > 4 {
401 guard = true;
402 break;
403 }
404 steps.push(if bit == 0 {
405 ThirdStep::Minor
406 } else {
407 ThirdStep::Major
408 });
409 index += 1;
410 if steps.len() >= 8 {
411 break;
412 }
413 }
414 let signature = Self { root, steps, guard };
415 signature.validate()?;
416 Ok(signature)
417 }
418
419 pub fn family_tag(&self) -> char {
422 let majors = self
423 .steps
424 .iter()
425 .filter(|step| matches!(step, ThirdStep::Major))
426 .count();
427 match majors {
428 0..=2 => 'w',
429 3 => 'x',
430 4 => 'y',
431 _ => 'z',
432 }
433 }
434
435 pub fn to_mask(&self) -> PitchClassMask {
438 let mut pitch_classes = vec![self.root];
439 let mut current = self.root;
440 for step in &self.steps {
441 current = current.transpose(match step {
442 ThirdStep::Minor => 3,
443 ThirdStep::Major => 4,
444 });
445 pitch_classes.push(current);
446 }
447 PitchClassMask::from_pitch_classes(&pitch_classes)
448 }
449}