Skip to main content

sim_lib_pitch_set/
model.rs

1use thiserror::Error;
2
3use sim_lib_pitch_core::{Pitch, PitchClass};
4
5/// Error returned when a pitch-set value cannot be constructed, encoded, or decoded.
6#[derive(Debug, Error, Clone, PartialEq, Eq)]
7pub enum PitchSetError {
8    /// A MIDI key outside the valid `0..128` range was supplied.
9    #[error("invalid MIDI key {0}")]
10    InvalidMidiKey(u8),
11    /// A pitch-class mask used bits outside the low twelve pitch-class bits.
12    #[error("invalid pitch-class mask {0}")]
13    InvalidPitchClassMask(u16),
14    /// A third-stack bit pattern could not be decoded into a valid signature.
15    #[error("invalid third stack encoding")]
16    InvalidThirdStackEncoding,
17    /// A third-stack signature violated the run-length constraints on its steps.
18    #[error("invalid third stack signature")]
19    InvalidThirdStack,
20}
21
22/// A bitmask over the twelve pitch classes, with bit `n` set when pitch class `n`
23/// is present.
24///
25/// The low twelve bits represent pitch classes C through B.
26///
27/// # Examples
28///
29/// ```
30/// use sim_lib_pitch_core::PitchClass;
31/// use sim_lib_pitch_set::PitchClassMask;
32///
33/// let triad = PitchClassMask::from_pitch_classes(&[PitchClass::C, PitchClass::E, PitchClass::G]);
34/// assert_eq!(triad.count_bits(), 3);
35/// ```
36#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
37pub struct PitchClassMask(u16);
38
39impl PitchClassMask {
40    const VALID_BITS: u16 = 0x0fff;
41
42    /// Builds a pitch-class mask, rejecting any bits outside the low twelve.
43    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    /// Returns the raw low-twelve pitch-class bits.
52    pub const fn bits(self) -> u16 {
53        self.0
54    }
55
56    /// Builds a mask from a slice of pitch classes; duplicates collapse to one bit.
57    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    /// Returns the set pitch classes in ascending order.
66    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    /// Returns this mask transposed by `semitones`, wrapping within the octave.
74    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    /// Returns this mask inverted about `axis`.
81    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    /// Returns the rotation of this mask with the smallest numeric value, a
90    /// transposition-invariant normal form.
91    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    /// Returns the number of pitch classes in the set (the population count).
99    pub fn count_bits(self) -> u32 {
100        self.0.count_ones()
101    }
102
103    /// Returns the [`IntervalVector`] tallying interval classes among the set's
104    /// pitch classes.
105    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/// A bitmask over the 128 MIDI keys, with bit `n` set when MIDI key `n` is present.
121///
122/// Unlike [`PitchClassMask`], this preserves octave, so it represents a concrete
123/// set of sounding pitches rather than pitch classes.
124#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
125pub struct PitchRangeMask {
126    /// The packed key bits, with bit `n` corresponding to MIDI key `n`.
127    pub bits: u128,
128}
129
130impl PitchRangeMask {
131    /// Adds `midi_key` to the set.
132    pub fn set(&mut self, midi_key: u8) {
133        self.bits |= 1u128 << midi_key;
134    }
135
136    /// Removes `midi_key` from the set.
137    pub fn clear(&mut self, midi_key: u8) {
138        self.bits &= !(1u128 << midi_key);
139    }
140
141    /// Returns `true` if `midi_key` is present in the set.
142    pub fn contains(self, midi_key: u8) -> bool {
143        self.bits & (1u128 << midi_key) != 0
144    }
145
146    /// Returns the union of this mask with `other`.
147    pub fn union(self, other: Self) -> Self {
148        Self {
149            bits: self.bits | other.bits,
150        }
151    }
152
153    /// Returns the intersection of this mask with `other`.
154    pub fn intersection(self, other: Self) -> Self {
155        Self {
156            bits: self.bits & other.bits,
157        }
158    }
159
160    /// Returns the keys present in this mask but not in `other`.
161    pub fn difference(self, other: Self) -> Self {
162        Self {
163            bits: self.bits & !other.bits,
164        }
165    }
166
167    /// Returns the set keys as concrete [`Pitch`] values in ascending order.
168    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/// An interval-class vector: counts of each of the six interval classes (1..=6)
177/// occurring among a set's pitch classes.
178#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
179pub struct IntervalVector(pub [u16; 6]);
180
181/// A chord represented as a [`PitchClassMask`] with an optional designated root.
182///
183/// When `root` is `None` the chord is rootless and can be reduced to its
184/// transposition-invariant normal form via [`BitChord::canonical`].
185#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
186pub struct BitChord {
187    /// The pitch classes that make up the chord.
188    pub mask: PitchClassMask,
189    /// The chord root, or `None` for a rootless chord.
190    pub root: Option<PitchClass>,
191}
192
193impl BitChord {
194    /// Returns a canonical form: rooted chords are returned unchanged, while
195    /// rootless chords are normalized to their lowest-valued rotation.
196    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/// A single step in a stack of thirds: a minor third (3 semitones) or major third
209/// (4 semitones).
210#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
211pub enum ThirdStep {
212    /// A minor third (3 semitones).
213    Minor,
214    /// A major third (4 semitones).
215    Major,
216}
217
218/// A chord described as a root pitch class plus an ordered stack of third [`ThirdStep`]s.
219///
220/// This tertian encoding captures triads, sevenths, and extended chords as a
221/// sequence of stacked thirds, with run-length limits that reject implausible
222/// stacks.
223#[derive(Clone, Debug, PartialEq, Eq, Hash)]
224pub struct ThirdStackSignature {
225    /// The pitch class at the bottom of the stack.
226    pub root: PitchClass,
227    /// The ordered thirds stacked above the root.
228    pub steps: Vec<ThirdStep>,
229    /// A guard bit reserved by the bit encoding to mark the end of the step run.
230    pub guard: bool,
231}
232
233impl ThirdStackSignature {
234    /// Validates the step run, rejecting four consecutive minor thirds or three
235    /// consecutive major thirds.
236    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    /// Encodes this signature into a compact `u32`, validating it first.
258    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    /// Decodes a `u32` produced by [`ThirdStackSignature::encode`] back into a
276    /// validated signature.
277    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    /// Returns a single-character family tag classifying the stack by its count of
306    /// major thirds.
307    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    /// Realizes the stacked thirds into the [`PitchClassMask`] of the chord's
322    /// pitch classes.
323    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}