use sim_lib_discrete_search::{
NeverInterrupt, SearchControl, SearchProblem, SearchRun, SearchStep, solve,
};
use sim_lib_pitch_core::PitchClass;
use sim_lib_pitch_set::{
PitchClassMask, PitchSetGraphError, PitchSetMovePolicy, PitchSetNeighborhood, PitchSetSpace,
ThirdStackSignature, ThirdStep,
};
use crate::PitchScaleError;
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TetrachordJoin {
Whole,
Half,
}
impl TetrachordJoin {
pub const fn semitones(self) -> u8 {
match self {
Self::Whole => 2,
Self::Half => 1,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Tetrachord {
pub offsets: [u8; 4],
}
impl Tetrachord {
pub fn new(offsets: [u8; 4]) -> Result<Self, PitchScaleError> {
if offsets[0] != 0 {
return Err(PitchScaleError::InvalidScaleInterval(offsets[0]));
}
for offset in offsets {
if offset >= 12 {
return Err(PitchScaleError::InvalidScaleInterval(offset));
}
}
if offsets.windows(2).any(|window| window[0] >= window[1]) {
return Err(PitchScaleError::InvalidScaleDegree(4));
}
Ok(Self { offsets })
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GeneratedScale {
pub ordinal: usize,
pub lower: Tetrachord,
pub upper: Tetrachord,
pub join: TetrachordJoin,
pub intervals: Vec<u8>,
pub mask: PitchClassMask,
pub third_stack: Option<ThirdStackSignature>,
pub fixture: Option<char>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ScaleFixture {
pub name: char,
pub third_stack_gaps: &'static [ThirdStep],
pub intervals: &'static [u8],
}
pub const SCALE_FIXTURE_W: ScaleFixture = ScaleFixture {
name: 'w',
third_stack_gaps: &[
ThirdStep::Major,
ThirdStep::Minor,
ThirdStep::Major,
ThirdStep::Minor,
ThirdStep::Minor,
ThirdStep::Major,
ThirdStep::Minor,
],
intervals: &[0, 2, 4, 5, 7, 9, 11],
};
pub const SCALE_FIXTURE_X: ScaleFixture = ScaleFixture {
name: 'x',
third_stack_gaps: &[
ThirdStep::Major,
ThirdStep::Minor,
ThirdStep::Major,
ThirdStep::Minor,
ThirdStep::Minor,
ThirdStep::Minor,
ThirdStep::Major,
],
intervals: &[0, 2, 4, 5, 7, 8, 11],
};
pub const SCALE_FIXTURE_Y: ScaleFixture = ScaleFixture {
name: 'y',
third_stack_gaps: &[
ThirdStep::Major,
ThirdStep::Major,
ThirdStep::Minor,
ThirdStep::Minor,
ThirdStep::Minor,
ThirdStep::Major,
ThirdStep::Minor,
],
intervals: &[0, 2, 4, 5, 8, 9, 11],
};
pub const SCALE_FIXTURE_Z: ScaleFixture = ScaleFixture {
name: 'z',
third_stack_gaps: &[
ThirdStep::Minor,
ThirdStep::Major,
ThirdStep::Major,
ThirdStep::Minor,
ThirdStep::Minor,
ThirdStep::Major,
ThirdStep::Minor,
],
intervals: &[0, 2, 3, 5, 7, 9, 11],
};
pub const SCALE_FIXTURES: &[ScaleFixture] = &[
SCALE_FIXTURE_W,
SCALE_FIXTURE_X,
SCALE_FIXTURE_Y,
SCALE_FIXTURE_Z,
];
pub fn generate_scales(
tetrachords: &[Tetrachord],
joins: &[TetrachordJoin],
control: SearchControl,
) -> SearchRun<GeneratedScale> {
solve(
&ScaleGenerationProblem { tetrachords, joins },
control,
&NeverInterrupt,
)
}
pub fn decode_scale_third_stack(intervals: &[u8]) -> Option<ThirdStackSignature> {
if intervals.len() != 7 || intervals.first().copied() != Some(0) {
return None;
}
let mut sorted = intervals.to_vec();
sorted.sort_unstable();
sorted.dedup();
if sorted != intervals {
return None;
}
let mut steps = Vec::new();
let mut index = 0usize;
for _ in 0..intervals.len() {
let next = (index + 2) % intervals.len();
let delta = (i16::from(intervals[next]) - i16::from(intervals[index])).rem_euclid(12);
steps.push(match delta {
3 => ThirdStep::Minor,
4 => ThirdStep::Major,
_ => return None,
});
index = next;
}
Some(ThirdStackSignature {
root: PitchClass::C,
steps,
guard: true,
})
}
pub fn nudge_scale_neighborhood(
scale: PitchClassMask,
move_policy: PitchSetMovePolicy,
control: SearchControl,
) -> Result<Vec<PitchClassMask>, PitchSetGraphError> {
let graph = PitchSetNeighborhood::new(
PitchSetSpace::chromatic(scale.count_bits() as u8),
move_policy,
)
.materialize(control)?;
let Some(source) = graph.nodes.iter().position(|candidate| *candidate == scale) else {
return Ok(Vec::new());
};
let mut neighbors = graph
.edges
.iter()
.filter_map(|edge| {
if edge.source == source {
Some(graph.nodes[edge.target])
} else if edge.target == source {
Some(graph.nodes[edge.source])
} else {
None
}
})
.collect::<Vec<_>>();
neighbors.sort_by_key(|mask| mask.bits());
neighbors.dedup();
Ok(neighbors)
}
struct ScaleGenerationProblem<'a> {
tetrachords: &'a [Tetrachord],
joins: &'a [TetrachordJoin],
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct ScaleGenerationState {
lower: Option<usize>,
upper: Option<usize>,
join: Option<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum ScaleGenerationChoice {
Lower(usize),
Upper(usize),
Join(usize),
}
impl SearchProblem for ScaleGenerationProblem<'_> {
type State = ScaleGenerationState;
type Choice = ScaleGenerationChoice;
type Output = GeneratedScale;
fn initial_state(&self) -> Self::State {
ScaleGenerationState::default()
}
fn expand(&self, state: &Self::State, out: &mut Vec<Self::Choice>) {
if state.lower.is_none() {
out.extend((0..self.tetrachords.len()).map(ScaleGenerationChoice::Lower));
} else if state.upper.is_none() {
out.extend((0..self.tetrachords.len()).map(ScaleGenerationChoice::Upper));
} else if state.join.is_none() {
out.extend((0..self.joins.len()).map(ScaleGenerationChoice::Join));
}
}
fn apply(&self, state: &Self::State, choice: &Self::Choice) -> SearchStep<Self::State> {
let mut next = state.clone();
match *choice {
ScaleGenerationChoice::Lower(index) if index < self.tetrachords.len() => {
next.lower = Some(index);
}
ScaleGenerationChoice::Upper(index) if index < self.tetrachords.len() => {
next.upper = Some(index);
}
ScaleGenerationChoice::Join(index) if index < self.joins.len() => {
next.join = Some(index);
}
_ => return SearchStep::infeasible("scale generation choice index outside axis"),
}
SearchStep::Continue(next)
}
fn finish(&self, state: &Self::State) -> Option<Self::Output> {
let lower_index = state.lower?;
let upper_index = state.upper?;
let join_index = state.join?;
let lower = self.tetrachords[lower_index];
let upper = self.tetrachords[upper_index];
let join = self.joins[join_index];
let upper_root = lower.offsets[3].checked_add(join.semitones())?;
let mut intervals = lower.offsets.to_vec();
for offset in upper.offsets {
let interval = upper_root.checked_add(offset)?;
if interval == 12 {
continue;
}
if interval > 12 {
return None;
}
intervals.push(interval);
}
intervals.sort_unstable();
intervals.dedup();
if intervals.len() < 2 {
return None;
}
let pitch_classes = intervals
.iter()
.map(|offset| PitchClass::C.transpose(i32::from(*offset)))
.collect::<Vec<_>>();
let mask = PitchClassMask::from_pitch_classes(&pitch_classes);
let third_stack = decode_scale_third_stack(&intervals);
let fixture = fixture_for(&intervals, third_stack.as_ref());
Some(GeneratedScale {
ordinal: lower_index * self.tetrachords.len() * self.joins.len()
+ upper_index * self.joins.len()
+ join_index,
lower,
upper,
join,
intervals,
mask,
third_stack,
fixture,
})
}
}
fn fixture_for(intervals: &[u8], stack: Option<&ThirdStackSignature>) -> Option<char> {
let stack = stack?;
SCALE_FIXTURES
.iter()
.find(|fixture| fixture.intervals == intervals && fixture.third_stack_gaps == stack.steps)
.map(|fixture| fixture.name)
}