use sim_lib_pitch_core::{Pitch, PitchClass};
use crate::{Mode, PitchScaleError, Scale};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct PlayerScale {
pub tonic: PitchClass,
intervals: Vec<u8>,
}
impl PlayerScale {
pub fn from_scale(scale: Scale) -> Self {
Self {
tonic: scale.tonic,
intervals: scale.mode.intervals().to_vec(),
}
}
pub fn from_key(tonic: PitchClass, mode: Mode) -> Self {
Self::from_scale(Scale::new(tonic, mode))
}
pub fn custom(
tonic: PitchClass,
intervals: impl Into<Vec<u8>>,
) -> Result<Self, PitchScaleError> {
let mut intervals = intervals.into();
if intervals.is_empty() {
return Err(PitchScaleError::EmptyScale);
}
for interval in &intervals {
if *interval >= 12 {
return Err(PitchScaleError::InvalidScaleInterval(*interval));
}
}
intervals.sort_unstable();
intervals.dedup();
Ok(Self { tonic, intervals })
}
pub fn intervals(&self) -> &[u8] {
&self.intervals
}
pub fn pitch_classes(&self) -> Vec<PitchClass> {
self.intervals
.iter()
.map(|interval| self.tonic.transpose(i32::from(*interval)))
.collect()
}
pub fn contains(&self, class: PitchClass) -> bool {
self.degree_of(class).is_some()
}
pub fn degree_of(&self, class: PitchClass) -> Option<usize> {
self.pitch_classes()
.iter()
.position(|candidate| *candidate == class)
.map(|index| index + 1)
}
pub fn pitch_at_degree(&self, degree: usize) -> Result<PitchClass, PitchScaleError> {
self.try_pitch_at_degree(degree)
}
pub fn try_pitch_at_degree(&self, degree: usize) -> Result<PitchClass, PitchScaleError> {
let index = degree
.checked_sub(1)
.ok_or(PitchScaleError::InvalidScaleDegree(degree))?;
Ok(self
.tonic
.transpose(i32::from(self.intervals[index % self.intervals.len()])))
}
pub fn nearest_pitch(&self, pitch: Pitch) -> Pitch {
let source = pitch.semitone();
self.pitch_classes()
.into_iter()
.flat_map(|class| {
(-1..=1).map(move |octave_offset| Pitch {
class,
octave: pitch.octave + octave_offset,
})
})
.min_by_key(|candidate| {
let delta = candidate.semitone() - source;
(delta.abs(), if delta >= 0 { 0 } else { 1 })
})
.unwrap_or(pitch)
}
pub fn remap_pitch(&self, pitch: Pitch) -> Pitch {
let offset =
(i32::from(pitch.class.value()) - i32::from(self.tonic.value())).rem_euclid(12);
let index =
usize::try_from(offset).expect("mod-12 offset fits usize") % self.intervals.len();
Pitch {
class: self.tonic.transpose(i32::from(self.intervals[index])),
octave: pitch.octave,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum ScaleLockPolicy {
Quantize,
Filter,
Remap,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ScaleLockPlayer {
pub scale: PlayerScale,
pub policy: ScaleLockPolicy,
}
impl ScaleLockPlayer {
pub fn new(scale: PlayerScale, policy: ScaleLockPolicy) -> Self {
Self { scale, policy }
}
pub fn from_scale(scale: Scale, policy: ScaleLockPolicy) -> Self {
Self::new(PlayerScale::from_scale(scale), policy)
}
pub fn process_pitch(&self, pitch: Pitch) -> Option<Pitch> {
match self.policy {
ScaleLockPolicy::Quantize => Some(self.scale.nearest_pitch(pitch)),
ScaleLockPolicy::Filter => self.scale.contains(pitch.class).then_some(pitch),
ScaleLockPolicy::Remap => Some(self.scale.remap_pitch(pitch)),
}
}
pub fn process_pitches(&self, pitches: impl IntoIterator<Item = Pitch>) -> Vec<Pitch> {
pitches
.into_iter()
.filter_map(|pitch| self.process_pitch(pitch))
.collect()
}
}