1use sim_lib_discrete_search::{
4 NeverInterrupt, SearchControl, SearchProblem, SearchRun, SearchStep, solve,
5};
6use sim_lib_pitch_core::PitchClass;
7use sim_lib_pitch_set::{
8 PitchClassMask, PitchSetGraphError, PitchSetMovePolicy, PitchSetNeighborhood, PitchSetSpace,
9 ThirdStackSignature, ThirdStep,
10};
11
12use crate::PitchScaleError;
13
14#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum TetrachordJoin {
17 Whole,
19 Half,
21}
22
23impl TetrachordJoin {
24 pub const fn semitones(self) -> u8 {
26 match self {
27 Self::Whole => 2,
28 Self::Half => 1,
29 }
30 }
31}
32
33#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
35pub struct Tetrachord {
36 pub offsets: [u8; 4],
38}
39
40impl Tetrachord {
41 pub fn new(offsets: [u8; 4]) -> Result<Self, PitchScaleError> {
43 if offsets[0] != 0 {
44 return Err(PitchScaleError::InvalidScaleInterval(offsets[0]));
45 }
46 for offset in offsets {
47 if offset >= 12 {
48 return Err(PitchScaleError::InvalidScaleInterval(offset));
49 }
50 }
51 if offsets.windows(2).any(|window| window[0] >= window[1]) {
52 return Err(PitchScaleError::InvalidScaleDegree(4));
53 }
54 Ok(Self { offsets })
55 }
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct GeneratedScale {
61 pub ordinal: usize,
63 pub lower: Tetrachord,
65 pub upper: Tetrachord,
67 pub join: TetrachordJoin,
69 pub intervals: Vec<u8>,
71 pub mask: PitchClassMask,
73 pub third_stack: Option<ThirdStackSignature>,
75 pub fixture: Option<char>,
77}
78
79#[derive(Copy, Clone, Debug, PartialEq, Eq)]
81pub struct ScaleFixture {
82 pub name: char,
84 pub third_stack_gaps: &'static [ThirdStep],
86 pub intervals: &'static [u8],
88}
89
90pub const SCALE_FIXTURE_W: ScaleFixture = ScaleFixture {
92 name: 'w',
93 third_stack_gaps: &[
94 ThirdStep::Major,
95 ThirdStep::Minor,
96 ThirdStep::Major,
97 ThirdStep::Minor,
98 ThirdStep::Minor,
99 ThirdStep::Major,
100 ThirdStep::Minor,
101 ],
102 intervals: &[0, 2, 4, 5, 7, 9, 11],
103};
104
105pub const SCALE_FIXTURE_X: ScaleFixture = ScaleFixture {
107 name: 'x',
108 third_stack_gaps: &[
109 ThirdStep::Major,
110 ThirdStep::Minor,
111 ThirdStep::Major,
112 ThirdStep::Minor,
113 ThirdStep::Minor,
114 ThirdStep::Minor,
115 ThirdStep::Major,
116 ],
117 intervals: &[0, 2, 4, 5, 7, 8, 11],
118};
119
120pub const SCALE_FIXTURE_Y: ScaleFixture = ScaleFixture {
122 name: 'y',
123 third_stack_gaps: &[
124 ThirdStep::Major,
125 ThirdStep::Major,
126 ThirdStep::Minor,
127 ThirdStep::Minor,
128 ThirdStep::Minor,
129 ThirdStep::Major,
130 ThirdStep::Minor,
131 ],
132 intervals: &[0, 2, 4, 5, 8, 9, 11],
133};
134
135pub const SCALE_FIXTURE_Z: ScaleFixture = ScaleFixture {
137 name: 'z',
138 third_stack_gaps: &[
139 ThirdStep::Minor,
140 ThirdStep::Major,
141 ThirdStep::Major,
142 ThirdStep::Minor,
143 ThirdStep::Minor,
144 ThirdStep::Major,
145 ThirdStep::Minor,
146 ],
147 intervals: &[0, 2, 3, 5, 7, 9, 11],
148};
149
150pub const SCALE_FIXTURES: &[ScaleFixture] = &[
152 SCALE_FIXTURE_W,
153 SCALE_FIXTURE_X,
154 SCALE_FIXTURE_Y,
155 SCALE_FIXTURE_Z,
156];
157
158pub fn generate_scales(
164 tetrachords: &[Tetrachord],
165 joins: &[TetrachordJoin],
166 control: SearchControl,
167) -> SearchRun<GeneratedScale> {
168 solve(
169 &ScaleGenerationProblem { tetrachords, joins },
170 control,
171 &NeverInterrupt,
172 )
173}
174
175pub fn decode_scale_third_stack(intervals: &[u8]) -> Option<ThirdStackSignature> {
177 if intervals.len() != 7 || intervals.first().copied() != Some(0) {
178 return None;
179 }
180 let mut sorted = intervals.to_vec();
181 sorted.sort_unstable();
182 sorted.dedup();
183 if sorted != intervals {
184 return None;
185 }
186 let mut steps = Vec::new();
187 let mut index = 0usize;
188 for _ in 0..intervals.len() {
189 let next = (index + 2) % intervals.len();
190 let delta = (i16::from(intervals[next]) - i16::from(intervals[index])).rem_euclid(12);
191 steps.push(match delta {
192 3 => ThirdStep::Minor,
193 4 => ThirdStep::Major,
194 _ => return None,
195 });
196 index = next;
197 }
198 Some(ThirdStackSignature {
199 root: PitchClass::C,
200 steps,
201 guard: true,
202 })
203}
204
205pub fn nudge_scale_neighborhood(
207 scale: PitchClassMask,
208 move_policy: PitchSetMovePolicy,
209 control: SearchControl,
210) -> Result<Vec<PitchClassMask>, PitchSetGraphError> {
211 let graph = PitchSetNeighborhood::new(
212 PitchSetSpace::chromatic(scale.count_bits() as u8),
213 move_policy,
214 )
215 .materialize(control)?;
216 let Some(source) = graph.nodes.iter().position(|candidate| *candidate == scale) else {
217 return Ok(Vec::new());
218 };
219 let mut neighbors = graph
220 .edges
221 .iter()
222 .filter_map(|edge| {
223 if edge.source == source {
224 Some(graph.nodes[edge.target])
225 } else if edge.target == source {
226 Some(graph.nodes[edge.source])
227 } else {
228 None
229 }
230 })
231 .collect::<Vec<_>>();
232 neighbors.sort_by_key(|mask| mask.bits());
233 neighbors.dedup();
234 Ok(neighbors)
235}
236
237struct ScaleGenerationProblem<'a> {
238 tetrachords: &'a [Tetrachord],
239 joins: &'a [TetrachordJoin],
240}
241
242#[derive(Clone, Debug, Default, PartialEq, Eq)]
243struct ScaleGenerationState {
244 lower: Option<usize>,
245 upper: Option<usize>,
246 join: Option<usize>,
247}
248
249#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
250enum ScaleGenerationChoice {
251 Lower(usize),
252 Upper(usize),
253 Join(usize),
254}
255
256impl SearchProblem for ScaleGenerationProblem<'_> {
257 type State = ScaleGenerationState;
258 type Choice = ScaleGenerationChoice;
259 type Output = GeneratedScale;
260
261 fn initial_state(&self) -> Self::State {
262 ScaleGenerationState::default()
263 }
264
265 fn expand(&self, state: &Self::State, out: &mut Vec<Self::Choice>) {
266 if state.lower.is_none() {
267 out.extend((0..self.tetrachords.len()).map(ScaleGenerationChoice::Lower));
268 } else if state.upper.is_none() {
269 out.extend((0..self.tetrachords.len()).map(ScaleGenerationChoice::Upper));
270 } else if state.join.is_none() {
271 out.extend((0..self.joins.len()).map(ScaleGenerationChoice::Join));
272 }
273 }
274
275 fn apply(&self, state: &Self::State, choice: &Self::Choice) -> SearchStep<Self::State> {
276 let mut next = state.clone();
277 match *choice {
278 ScaleGenerationChoice::Lower(index) if index < self.tetrachords.len() => {
279 next.lower = Some(index);
280 }
281 ScaleGenerationChoice::Upper(index) if index < self.tetrachords.len() => {
282 next.upper = Some(index);
283 }
284 ScaleGenerationChoice::Join(index) if index < self.joins.len() => {
285 next.join = Some(index);
286 }
287 _ => return SearchStep::infeasible("scale generation choice index outside axis"),
288 }
289 SearchStep::Continue(next)
290 }
291
292 fn finish(&self, state: &Self::State) -> Option<Self::Output> {
293 let lower_index = state.lower?;
294 let upper_index = state.upper?;
295 let join_index = state.join?;
296 let lower = self.tetrachords[lower_index];
297 let upper = self.tetrachords[upper_index];
298 let join = self.joins[join_index];
299 let upper_root = lower.offsets[3].checked_add(join.semitones())?;
300 let mut intervals = lower.offsets.to_vec();
301 for offset in upper.offsets {
302 let interval = upper_root.checked_add(offset)?;
303 if interval == 12 {
304 continue;
305 }
306 if interval > 12 {
307 return None;
308 }
309 intervals.push(interval);
310 }
311 intervals.sort_unstable();
312 intervals.dedup();
313 if intervals.len() < 2 {
314 return None;
315 }
316 let pitch_classes = intervals
317 .iter()
318 .map(|offset| PitchClass::C.transpose(i32::from(*offset)))
319 .collect::<Vec<_>>();
320 let mask = PitchClassMask::from_pitch_classes(&pitch_classes);
321 let third_stack = decode_scale_third_stack(&intervals);
322 let fixture = fixture_for(&intervals, third_stack.as_ref());
323 Some(GeneratedScale {
324 ordinal: lower_index * self.tetrachords.len() * self.joins.len()
325 + upper_index * self.joins.len()
326 + join_index,
327 lower,
328 upper,
329 join,
330 intervals,
331 mask,
332 third_stack,
333 fixture,
334 })
335 }
336}
337
338fn fixture_for(intervals: &[u8], stack: Option<&ThirdStackSignature>) -> Option<char> {
339 let stack = stack?;
340 SCALE_FIXTURES
341 .iter()
342 .find(|fixture| fixture.intervals == intervals && fixture.third_stack_gaps == stack.steps)
343 .map(|fixture| fixture.name)
344}