Skip to main content

sim_lib_pitch_serial/
affine.rs

1//! Ordinal and affine transforms over strict tone rows.
2
3use std::collections::BTreeMap;
4
5use sim_lib_pitch_core::PitchClass;
6use sim_lib_serial_core::OrdinalMap;
7
8use crate::{
9    BlockProjection, BlockProjectionSource, OrderedPitchBlock, PitchReservoir, RowError, ToneRow,
10};
11
12/// Result of a pitch transform that may or may not preserve strict row identity.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub enum PitchTransformOutput {
15    /// The transform preserved a strict tone row.
16    Row(ToneRow),
17    /// The transform relaxed strict row invariants into an ordered reservoir.
18    Reservoir(PitchReservoir),
19}
20
21/// An affine pitch-class map `x -> ax + b mod 12`.
22#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
23pub struct AffinePitchMap {
24    /// Multiplicative factor `a`, reduced modulo twelve.
25    pub multiplier: u8,
26    /// Additive factor `b`, reduced modulo twelve.
27    pub addend: u8,
28}
29
30impl AffinePitchMap {
31    /// Constructs an affine pitch-class map with canonical modulo-twelve factors.
32    pub const fn new(multiplier: u8, addend: u8) -> Self {
33        Self {
34            multiplier: multiplier % 12,
35            addend: addend % 12,
36        }
37    }
38
39    /// Returns `true` exactly when the map is bijective over pitch classes.
40    pub const fn is_bijective(self) -> bool {
41        matches!(self.multiplier % 12, 1 | 5 | 7 | 11)
42    }
43
44    /// Applies the affine map to `row`, returning a strict row only for bijections.
45    pub fn apply(self, row: &ToneRow) -> PitchTransformOutput {
46        let mapped = row
47            .classes()
48            .map(|pitch_class| self.map_pitch_class(pitch_class));
49        if self.is_bijective() {
50            PitchTransformOutput::Row(ToneRow::from_valid_classes(mapped))
51        } else {
52            PitchTransformOutput::Reservoir(self.into_reservoir(row, mapped))
53        }
54    }
55
56    fn map_pitch_class(self, pitch_class: PitchClass) -> PitchClass {
57        from_mod12(
58            (u16::from(self.multiplier) * u16::from(pitch_class.value()) + u16::from(self.addend))
59                % 12,
60        )
61    }
62
63    fn into_reservoir(self, row: &ToneRow, mapped: [PitchClass; 12]) -> PitchReservoir {
64        let mut ordinals_by_pitch = BTreeMap::<u8, Vec<u8>>::new();
65        for (ordinal, pitch_class) in mapped.iter().enumerate() {
66            ordinals_by_pitch
67                .entry(pitch_class.value())
68                .or_default()
69                .push(ordinal as u8);
70        }
71        let mut blocks = Vec::with_capacity(ordinals_by_pitch.len());
72        let mut provenance = Vec::with_capacity(ordinals_by_pitch.len());
73        for (block_index, (pitch_value, ordinals)) in ordinals_by_pitch.into_iter().enumerate() {
74            let target_pitch_class = from_mod12(u16::from(pitch_value));
75            let pitch_classes = ordinals
76                .iter()
77                .map(|ordinal| row.classes()[usize::from(*ordinal)])
78                .map(|pitch_class| self.map_pitch_class(pitch_class))
79                .collect::<Vec<_>>();
80            blocks.push(OrderedPitchBlock {
81                mask: sim_lib_pitch_set::PitchClassMask::from_pitch_classes(&pitch_classes),
82                pitch_classes,
83            });
84            provenance.push(BlockProjection {
85                block_index,
86                source: BlockProjectionSource::OrdinalCollapse {
87                    source_ordinals: ordinals,
88                    target_pitch_class,
89                },
90            });
91        }
92        PitchReservoir::new(blocks, provenance)
93    }
94}
95
96impl ToneRow {
97    /// Returns the row rotated left by `steps`, reduced modulo twelve.
98    pub fn rotate(&self, steps: usize) -> Self {
99        self.permute_ordinals(&OrdinalMap::rotation(12, steps))
100            .expect("fixed-cardinality rotation is always valid")
101    }
102
103    /// Applies a caller-supplied validated ordinal permutation to the row.
104    pub fn permute_ordinals(&self, permutation: &OrdinalMap) -> Result<Self, RowError> {
105        let classes = permutation.apply(self.classes())?;
106        let classes = std::array::from_fn(|index| classes[index]);
107        Ok(Self::from_valid_classes(classes))
108    }
109
110    /// Validates and applies a raw output-to-input ordinal permutation.
111    pub fn try_permute_ordinals(&self, output_to_input: Vec<usize>) -> Result<Self, RowError> {
112        self.permute_ordinals(&OrdinalMap::try_new(output_to_input)?)
113    }
114}
115
116fn from_mod12(value: u16) -> PitchClass {
117    match value % 12 {
118        0 => PitchClass::C,
119        1 => PitchClass::CS,
120        2 => PitchClass::D,
121        3 => PitchClass::DS,
122        4 => PitchClass::E,
123        5 => PitchClass::F,
124        6 => PitchClass::FS,
125        7 => PitchClass::G,
126        8 => PitchClass::GS,
127        9 => PitchClass::A,
128        10 => PitchClass::AS,
129        11 => PitchClass::B,
130        _ => unreachable!(),
131    }
132}