sim_lib_pitch_serial/
affine.rs1use 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#[derive(Clone, Debug, PartialEq, Eq)]
14pub enum PitchTransformOutput {
15 Row(ToneRow),
17 Reservoir(PitchReservoir),
19}
20
21#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
23pub struct AffinePitchMap {
24 pub multiplier: u8,
26 pub addend: u8,
28}
29
30impl AffinePitchMap {
31 pub const fn new(multiplier: u8, addend: u8) -> Self {
33 Self {
34 multiplier: multiplier % 12,
35 addend: addend % 12,
36 }
37 }
38
39 pub const fn is_bijective(self) -> bool {
41 matches!(self.multiplier % 12, 1 | 5 | 7 | 11)
42 }
43
44 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 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 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 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}