use thiserror::Error;
use sim_lib_pitch_core::{Pitch, PitchClass};
use crate::{SetClass, SetEquivalence, classify_set, conventional};
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum PitchSetError {
#[error("invalid MIDI key {0}")]
InvalidMidiKey(u8),
#[error("invalid pitch-class mask {0}")]
InvalidPitchClassMask(u16),
#[error("invalid third stack encoding")]
InvalidThirdStackEncoding,
#[error("invalid third stack signature")]
InvalidThirdStack,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
pub struct PitchClassMask(u16);
impl PitchClassMask {
const VALID_BITS: u16 = 0x0fff;
pub fn new(bits: u16) -> Result<Self, PitchSetError> {
if bits & !Self::VALID_BITS == 0 {
Ok(Self(bits))
} else {
Err(PitchSetError::InvalidPitchClassMask(bits))
}
}
pub const fn bits(self) -> u16 {
self.0
}
pub fn from_pitch_classes(pitch_classes: &[PitchClass]) -> Self {
let mut bits = 0u16;
for pitch_class in pitch_classes {
bits |= 1u16 << pitch_class.value();
}
Self(bits)
}
pub fn pitch_classes(self) -> Vec<PitchClass> {
(0..12)
.filter(|bit| self.0 & (1u16 << bit) != 0)
.map(|bit| PitchClass::new(bit).expect("mask iteration yields valid pitch classes"))
.collect()
}
pub fn rotate(self, semitones: i32) -> Self {
let shift = semitones.rem_euclid(12) as u32;
let bits = self.0;
Self(((bits << shift) | (bits >> (12 - shift))) & Self::VALID_BITS)
}
pub fn invert(self, axis: PitchClass) -> Self {
let mut out = 0u16;
for pitch_class in self.pitch_classes() {
out |= 1u16 << pitch_class.invert(axis).value();
}
Self(out)
}
pub fn invert_tni(self, index: u8) -> Self {
let index = i32::from(index % 12);
let classes: Vec<_> = self
.pitch_classes()
.into_iter()
.map(|pitch_class| {
PitchClass::new((index - i32::from(pitch_class.value())).rem_euclid(12) as u8)
.expect("TnI folds to a valid pitch class")
})
.collect();
Self::from_pitch_classes(&classes)
}
pub fn normalize(self) -> Self {
(0..12)
.map(|shift| self.rotate(-shift))
.min_by_key(|mask| mask.bits())
.unwrap_or(self)
}
pub fn normal_order(self) -> Vec<PitchClass> {
conventional::normal_order(self.pitch_classes())
}
pub fn classify(self, equivalence: SetEquivalence) -> SetClass {
classify_set(self, equivalence)
}
pub fn is_subset_of(self, other: Self) -> bool {
self.0 & !other.0 == 0
}
pub fn is_superset_of(self, other: Self) -> bool {
other.is_subset_of(self)
}
pub fn complement(self) -> Self {
Self(!self.0 & Self::VALID_BITS)
}
pub fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
pub fn intersection(self, other: Self) -> Self {
Self(self.0 & other.0)
}
pub fn difference(self, other: Self) -> Self {
Self(self.0 & !other.0)
}
pub fn symmetric_difference(self, other: Self) -> Self {
Self(self.0 ^ other.0)
}
pub fn is_disjoint_from(self, other: Self) -> bool {
self.intersection(other).bits() == 0
}
pub fn transpositional_symmetries(self) -> Vec<u8> {
(0..12)
.filter(|shift| self.rotate(i32::from(*shift)) == self)
.collect()
}
pub fn inversional_symmetries(self) -> Vec<PitchClass> {
(0..12)
.filter_map(|axis| {
let pitch_class =
PitchClass::new(axis).expect("symmetry axis iteration yields pitch classes");
(self.invert(pitch_class) == self).then_some(pitch_class)
})
.collect()
}
pub fn roots(self) -> Vec<PitchClass> {
let mut roots = Vec::new();
for root in self.pitch_classes() {
let contains = |semitones| self.0 & (1u16 << root.transpose(semitones).value()) != 0;
if contains(7) && (contains(3) || contains(4)) {
roots.push(root);
}
}
roots
}
pub fn is_z_related_to(self, other: Self) -> bool {
self.count_bits() == other.count_bits()
&& self.interval_vector() == other.interval_vector()
&& classify_set(self, SetEquivalence::TranspositionInversion).prime
!= classify_set(other, SetEquivalence::TranspositionInversion).prime
}
pub fn count_bits(self) -> u32 {
self.0.count_ones()
}
pub fn interval_vector(self) -> IntervalVector {
let pitch_classes = self.pitch_classes();
let mut bins = [0u16; 6];
for (index, a) in pitch_classes.iter().enumerate() {
for b in pitch_classes.iter().skip(index + 1) {
let class = a.interval_class(*b);
if class > 0 {
bins[(class - 1) as usize] += 1;
}
}
}
IntervalVector(bins)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
pub struct PitchRangeMask {
pub bits: u128,
}
impl PitchRangeMask {
pub fn set(&mut self, midi_key: u8) {
self.bits |= 1u128 << midi_key;
}
pub fn clear(&mut self, midi_key: u8) {
self.bits &= !(1u128 << midi_key);
}
pub fn contains(self, midi_key: u8) -> bool {
self.bits & (1u128 << midi_key) != 0
}
pub fn union(self, other: Self) -> Self {
Self {
bits: self.bits | other.bits,
}
}
pub fn intersection(self, other: Self) -> Self {
Self {
bits: self.bits & other.bits,
}
}
pub fn difference(self, other: Self) -> Self {
Self {
bits: self.bits & !other.bits,
}
}
pub fn to_pitches(self) -> Vec<Pitch> {
(0..128u8)
.filter(|key| self.contains(*key))
.map(Pitch::from_midi)
.collect()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct IntervalVector(pub [u16; 6]);
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct BitChord {
pub mask: PitchClassMask,
pub root: Option<PitchClass>,
}
impl BitChord {
pub fn canonical(self) -> Self {
if self.root.is_some() {
self
} else {
Self {
mask: self.mask.normalize(),
root: None,
}
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum ThirdStep {
Minor,
Major,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ThirdStackSignature {
pub root: PitchClass,
pub steps: Vec<ThirdStep>,
pub guard: bool,
}
impl ThirdStackSignature {
pub fn validate(&self) -> Result<(), PitchSetError> {
let mut minor_run = 0usize;
let mut major_run = 0usize;
for step in &self.steps {
match step {
ThirdStep::Minor => {
minor_run += 1;
major_run = 0;
}
ThirdStep::Major => {
major_run += 1;
minor_run = 0;
}
}
if minor_run >= 4 || major_run >= 3 {
return Err(PitchSetError::InvalidThirdStack);
}
}
Ok(())
}
pub fn encode(&self) -> Result<u32, PitchSetError> {
self.validate()?;
let mut encoded = u32::from(self.root.value());
for (index, step) in self.steps.iter().enumerate() {
let bit = if matches!(step, ThirdStep::Major) {
1u32
} else {
0u32
};
encoded |= bit << (4 + index);
}
if self.guard {
encoded |= 1u32 << (4 + self.steps.len());
}
Ok(encoded)
}
pub fn decode(encoded: u32) -> Result<Self, PitchSetError> {
let root =
PitchClass::new(u8::try_from(encoded & 0x0f).expect("third-stack root nibble fits u8"))
.map_err(|_| PitchSetError::InvalidThirdStackEncoding)?;
let mut steps = Vec::new();
let mut index = 4u32;
let mut guard = false;
while index < 31 {
let bit = (encoded >> index) & 1;
if ((encoded >> (index + 1)) & 1) == 0 && bit == 1 && index > 4 {
guard = true;
break;
}
steps.push(if bit == 0 {
ThirdStep::Minor
} else {
ThirdStep::Major
});
index += 1;
if steps.len() >= 8 {
break;
}
}
let signature = Self { root, steps, guard };
signature.validate()?;
Ok(signature)
}
pub fn family_tag(&self) -> char {
let majors = self
.steps
.iter()
.filter(|step| matches!(step, ThirdStep::Major))
.count();
match majors {
0..=2 => 'w',
3 => 'x',
4 => 'y',
_ => 'z',
}
}
pub fn to_mask(&self) -> PitchClassMask {
let mut pitch_classes = vec![self.root];
let mut current = self.root;
for step in &self.steps {
current = current.transpose(match step {
ThirdStep::Minor => 3,
ThirdStep::Major => 4,
});
pitch_classes.push(current);
}
PitchClassMask::from_pitch_classes(&pitch_classes)
}
}