sim_lib_pitch_scale/
player.rs1use sim_lib_pitch_core::{Pitch, PitchClass};
2
3use crate::{Mode, PitchScaleError, Scale};
4
5#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub struct PlayerScale {
13 pub tonic: PitchClass,
15 intervals: Vec<u8>,
16}
17
18impl PlayerScale {
19 pub fn from_scale(scale: Scale) -> Self {
21 Self {
22 tonic: scale.tonic,
23 intervals: scale.mode.intervals().to_vec(),
24 }
25 }
26
27 pub fn from_key(tonic: PitchClass, mode: Mode) -> Self {
29 Self::from_scale(Scale::new(tonic, mode))
30 }
31
32 pub fn custom(
38 tonic: PitchClass,
39 intervals: impl Into<Vec<u8>>,
40 ) -> Result<Self, PitchScaleError> {
41 let mut intervals = intervals.into();
42 if intervals.is_empty() {
43 return Err(PitchScaleError::EmptyScale);
44 }
45 for interval in &intervals {
46 if *interval >= 12 {
47 return Err(PitchScaleError::InvalidScaleInterval(*interval));
48 }
49 }
50 intervals.sort_unstable();
51 intervals.dedup();
52 Ok(Self { tonic, intervals })
53 }
54
55 pub fn intervals(&self) -> &[u8] {
57 &self.intervals
58 }
59
60 pub fn pitch_classes(&self) -> Vec<PitchClass> {
62 self.intervals
63 .iter()
64 .map(|interval| self.tonic.transpose(i32::from(*interval)))
65 .collect()
66 }
67
68 pub fn contains(&self, class: PitchClass) -> bool {
70 self.degree_of(class).is_some()
71 }
72
73 pub fn degree_of(&self, class: PitchClass) -> Option<usize> {
76 self.pitch_classes()
77 .iter()
78 .position(|candidate| *candidate == class)
79 .map(|index| index + 1)
80 }
81
82 pub fn pitch_at_degree(&self, degree: usize) -> Result<PitchClass, PitchScaleError> {
84 self.try_pitch_at_degree(degree)
85 }
86
87 pub fn try_pitch_at_degree(&self, degree: usize) -> Result<PitchClass, PitchScaleError> {
89 let index = degree
90 .checked_sub(1)
91 .ok_or(PitchScaleError::InvalidScaleDegree(degree))?;
92 Ok(self
93 .tonic
94 .transpose(i32::from(self.intervals[index % self.intervals.len()])))
95 }
96
97 pub fn nearest_pitch(&self, pitch: Pitch) -> Pitch {
99 let source = pitch.semitone();
100 self.pitch_classes()
101 .into_iter()
102 .flat_map(|class| {
103 (-1..=1).map(move |octave_offset| Pitch {
104 class,
105 octave: pitch.octave + octave_offset,
106 })
107 })
108 .min_by_key(|candidate| {
109 let delta = candidate.semitone() - source;
110 (delta.abs(), if delta >= 0 { 0 } else { 1 })
111 })
112 .unwrap_or(pitch)
113 }
114
115 pub fn remap_pitch(&self, pitch: Pitch) -> Pitch {
118 let offset =
119 (i32::from(pitch.class.value()) - i32::from(self.tonic.value())).rem_euclid(12);
120 let index =
121 usize::try_from(offset).expect("mod-12 offset fits usize") % self.intervals.len();
122 Pitch {
123 class: self.tonic.transpose(i32::from(self.intervals[index])),
124 octave: pitch.octave,
125 }
126 }
127}
128
129#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
131pub enum ScaleLockPolicy {
132 Quantize,
134 Filter,
136 Remap,
138}
139
140#[derive(Clone, Debug, PartialEq, Eq, Hash)]
142pub struct ScaleLockPlayer {
143 pub scale: PlayerScale,
145 pub policy: ScaleLockPolicy,
147}
148
149impl ScaleLockPlayer {
150 pub fn new(scale: PlayerScale, policy: ScaleLockPolicy) -> Self {
152 Self { scale, policy }
153 }
154
155 pub fn from_scale(scale: Scale, policy: ScaleLockPolicy) -> Self {
157 Self::new(PlayerScale::from_scale(scale), policy)
158 }
159
160 pub fn process_pitch(&self, pitch: Pitch) -> Option<Pitch> {
163 match self.policy {
164 ScaleLockPolicy::Quantize => Some(self.scale.nearest_pitch(pitch)),
165 ScaleLockPolicy::Filter => self.scale.contains(pitch.class).then_some(pitch),
166 ScaleLockPolicy::Remap => Some(self.scale.remap_pitch(pitch)),
167 }
168 }
169
170 pub fn process_pitches(&self, pitches: impl IntoIterator<Item = Pitch>) -> Vec<Pitch> {
173 pitches
174 .into_iter()
175 .filter_map(|pitch| self.process_pitch(pitch))
176 .collect()
177 }
178}