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 third-stack bit pattern could not be decoded into a valid signature.
12    #[error("invalid third stack encoding")]
13    InvalidThirdStackEncoding,
14    /// A third-stack signature violated the run-length constraints on its steps.
15    #[error("invalid third stack signature")]
16    InvalidThirdStack,
17}
18
19/// A bitmask over the twelve pitch classes, with bit `n` set when pitch class `n`
20/// is present.
21///
22/// The low twelve bits of the inner `u16` represent pitch classes C through B.
23///
24/// # Examples
25///
26/// ```
27/// use sim_lib_pitch_core::PitchClass;
28/// use sim_lib_pitch_set::PitchClassMask;
29///
30/// let triad = PitchClassMask::from_pitch_classes(&[PitchClass::C, PitchClass::E, PitchClass::G]);
31/// assert_eq!(triad.count_bits(), 3);
32/// ```
33#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
34pub struct PitchClassMask(pub u16);
35
36impl PitchClassMask {
37    /// Builds a mask from a slice of pitch classes; duplicates collapse to one bit.
38    pub fn from_pitch_classes(pitch_classes: &[PitchClass]) -> Self {
39        let mut bits = 0u16;
40        for pitch_class in pitch_classes {
41            bits |= 1u16 << pitch_class.0;
42        }
43        Self(bits)
44    }
45
46    /// Returns the set pitch classes in ascending order.
47    pub fn pitch_classes(self) -> Vec<PitchClass> {
48        (0..12)
49            .filter(|bit| self.0 & (1u16 << bit) != 0)
50            .map(|bit| PitchClass(bit as u8))
51            .collect()
52    }
53
54    /// Returns this mask transposed by `semitones`, wrapping within the octave.
55    pub fn rotate(self, semitones: i32) -> Self {
56        let shift = semitones.rem_euclid(12) as u32;
57        let bits = self.0 & 0x0fff;
58        Self(((bits << shift) | (bits >> (12 - shift))) & 0x0fff)
59    }
60
61    /// Returns this mask inverted about `axis`.
62    pub fn invert(self, axis: PitchClass) -> Self {
63        let mut out = 0u16;
64        for pitch_class in self.pitch_classes() {
65            out |= 1u16 << pitch_class.invert(axis).0;
66        }
67        Self(out)
68    }
69
70    /// Returns the rotation of this mask with the smallest numeric value, a
71    /// transposition-invariant normal form.
72    pub fn normalize(self) -> Self {
73        (0..12)
74            .map(|shift| self.rotate(-shift))
75            .min_by_key(|mask| mask.0)
76            .unwrap_or(self)
77    }
78
79    /// Returns the number of pitch classes in the set (the population count).
80    pub fn count_bits(self) -> u32 {
81        self.0.count_ones()
82    }
83
84    /// Returns the [`IntervalVector`] tallying interval classes among the set's
85    /// pitch classes.
86    pub fn interval_vector(self) -> IntervalVector {
87        let pitch_classes = self.pitch_classes();
88        let mut bins = [0u16; 6];
89        for (index, a) in pitch_classes.iter().enumerate() {
90            for b in pitch_classes.iter().skip(index + 1) {
91                let class = a.interval_class(*b);
92                if class > 0 {
93                    bins[(class - 1) as usize] += 1;
94                }
95            }
96        }
97        IntervalVector(bins)
98    }
99}
100
101/// A bitmask over the 128 MIDI keys, with bit `n` set when MIDI key `n` is present.
102///
103/// Unlike [`PitchClassMask`], this preserves octave, so it represents a concrete
104/// set of sounding pitches rather than pitch classes.
105#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
106pub struct PitchRangeMask {
107    /// The packed key bits, with bit `n` corresponding to MIDI key `n`.
108    pub bits: u128,
109}
110
111impl PitchRangeMask {
112    /// Adds `midi_key` to the set.
113    pub fn set(&mut self, midi_key: u8) {
114        self.bits |= 1u128 << midi_key;
115    }
116
117    /// Removes `midi_key` from the set.
118    pub fn clear(&mut self, midi_key: u8) {
119        self.bits &= !(1u128 << midi_key);
120    }
121
122    /// Returns `true` if `midi_key` is present in the set.
123    pub fn contains(self, midi_key: u8) -> bool {
124        self.bits & (1u128 << midi_key) != 0
125    }
126
127    /// Returns the union of this mask with `other`.
128    pub fn union(self, other: Self) -> Self {
129        Self {
130            bits: self.bits | other.bits,
131        }
132    }
133
134    /// Returns the intersection of this mask with `other`.
135    pub fn intersection(self, other: Self) -> Self {
136        Self {
137            bits: self.bits & other.bits,
138        }
139    }
140
141    /// Returns the keys present in this mask but not in `other`.
142    pub fn difference(self, other: Self) -> Self {
143        Self {
144            bits: self.bits & !other.bits,
145        }
146    }
147
148    /// Returns the set keys as concrete [`Pitch`] values in ascending order.
149    pub fn to_pitches(self) -> Vec<Pitch> {
150        (0..128u8)
151            .filter(|key| self.contains(*key))
152            .map(Pitch::from_midi)
153            .collect()
154    }
155}
156
157/// An interval-class vector: counts of each of the six interval classes (1..=6)
158/// occurring among a set's pitch classes.
159#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
160pub struct IntervalVector(pub [u16; 6]);
161
162/// A chord represented as a [`PitchClassMask`] with an optional designated root.
163///
164/// When `root` is `None` the chord is rootless and can be reduced to its
165/// transposition-invariant normal form via [`BitChord::canonical`].
166#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
167pub struct BitChord {
168    /// The pitch classes that make up the chord.
169    pub mask: PitchClassMask,
170    /// The chord root, or `None` for a rootless chord.
171    pub root: Option<PitchClass>,
172}
173
174impl BitChord {
175    /// Returns a canonical form: rooted chords are returned unchanged, while
176    /// rootless chords are normalized to their lowest-valued rotation.
177    pub fn canonical(self) -> Self {
178        if self.root.is_some() {
179            self
180        } else {
181            Self {
182                mask: self.mask.normalize(),
183                root: None,
184            }
185        }
186    }
187}
188
189/// A single step in a stack of thirds: a minor third (3 semitones) or major third
190/// (4 semitones).
191#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
192pub enum ThirdStep {
193    /// A minor third (3 semitones).
194    Minor,
195    /// A major third (4 semitones).
196    Major,
197}
198
199/// A chord described as a root pitch class plus an ordered stack of third [`ThirdStep`]s.
200///
201/// This tertian encoding captures triads, sevenths, and extended chords as a
202/// sequence of stacked thirds, with run-length limits that reject implausible
203/// stacks.
204#[derive(Clone, Debug, PartialEq, Eq, Hash)]
205pub struct ThirdStackSignature {
206    /// The pitch class at the bottom of the stack.
207    pub root: PitchClass,
208    /// The ordered thirds stacked above the root.
209    pub steps: Vec<ThirdStep>,
210    /// A guard bit reserved by the bit encoding to mark the end of the step run.
211    pub guard: bool,
212}
213
214impl ThirdStackSignature {
215    /// Validates the step run, rejecting four consecutive minor thirds or three
216    /// consecutive major thirds.
217    pub fn validate(&self) -> Result<(), PitchSetError> {
218        let mut minor_run = 0usize;
219        let mut major_run = 0usize;
220        for step in &self.steps {
221            match step {
222                ThirdStep::Minor => {
223                    minor_run += 1;
224                    major_run = 0;
225                }
226                ThirdStep::Major => {
227                    major_run += 1;
228                    minor_run = 0;
229                }
230            }
231            if minor_run >= 4 || major_run >= 3 {
232                return Err(PitchSetError::InvalidThirdStack);
233            }
234        }
235        Ok(())
236    }
237
238    /// Encodes this signature into a compact `u32`, validating it first.
239    pub fn encode(&self) -> Result<u32, PitchSetError> {
240        self.validate()?;
241        let mut encoded = self.root.0 as u32;
242        for (index, step) in self.steps.iter().enumerate() {
243            let bit = matches!(step, ThirdStep::Major) as u32;
244            encoded |= bit << (4 + index);
245        }
246        if self.guard {
247            encoded |= 1u32 << (4 + self.steps.len());
248        }
249        Ok(encoded)
250    }
251
252    /// Decodes a `u32` produced by [`ThirdStackSignature::encode`] back into a
253    /// validated signature.
254    pub fn decode(encoded: u32) -> Result<Self, PitchSetError> {
255        let root = PitchClass((encoded & 0x0f) as u8);
256        let mut steps = Vec::new();
257        let mut index = 4u32;
258        let mut guard = false;
259        while index < 31 {
260            let bit = (encoded >> index) & 1;
261            if ((encoded >> (index + 1)) & 1) == 0 && bit == 1 && index > 4 {
262                guard = true;
263                break;
264            }
265            steps.push(if bit == 0 {
266                ThirdStep::Minor
267            } else {
268                ThirdStep::Major
269            });
270            index += 1;
271            if steps.len() >= 8 {
272                break;
273            }
274        }
275        let signature = Self { root, steps, guard };
276        signature.validate()?;
277        Ok(signature)
278    }
279
280    /// Returns a single-character family tag classifying the stack by its count of
281    /// major thirds.
282    pub fn family_tag(&self) -> char {
283        let majors = self
284            .steps
285            .iter()
286            .filter(|step| matches!(step, ThirdStep::Major))
287            .count();
288        match majors {
289            0..=2 => 'w',
290            3 => 'x',
291            4 => 'y',
292            _ => 'z',
293        }
294    }
295
296    /// Realizes the stacked thirds into the [`PitchClassMask`] of the chord's
297    /// pitch classes.
298    pub fn to_mask(&self) -> PitchClassMask {
299        let mut pitch_classes = vec![self.root];
300        let mut current = self.root;
301        for step in &self.steps {
302            current = current.transpose(match step {
303                ThirdStep::Minor => 3,
304                ThirdStep::Major => 4,
305            });
306            pitch_classes.push(current);
307        }
308        PitchClassMask::from_pitch_classes(&pitch_classes)
309    }
310}