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) -> PitchClass {
84 let index = degree.saturating_sub(1) % self.intervals.len();
85 self.tonic.transpose(i32::from(self.intervals[index]))
86 }
87
88 pub fn nearest_pitch(&self, pitch: Pitch) -> Pitch {
90 let source = pitch.semitone();
91 self.pitch_classes()
92 .into_iter()
93 .flat_map(|class| {
94 (-1..=1).map(move |octave_offset| Pitch {
95 class,
96 octave: pitch.octave + octave_offset,
97 })
98 })
99 .min_by_key(|candidate| {
100 let delta = candidate.semitone() - source;
101 (delta.abs(), if delta >= 0 { 0 } else { 1 })
102 })
103 .unwrap_or(pitch)
104 }
105
106 pub fn remap_pitch(&self, pitch: Pitch) -> Pitch {
109 let offset = (i32::from(pitch.class.0) - i32::from(self.tonic.0)).rem_euclid(12);
110 let degree = offset as usize % self.intervals.len() + 1;
111 Pitch {
112 class: self.pitch_at_degree(degree),
113 octave: pitch.octave,
114 }
115 }
116}
117
118#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
120pub enum ScaleLockPolicy {
121 Quantize,
123 Filter,
125 Remap,
127}
128
129#[derive(Clone, Debug, PartialEq, Eq, Hash)]
131pub struct ScaleLockPlayer {
132 pub scale: PlayerScale,
134 pub policy: ScaleLockPolicy,
136}
137
138impl ScaleLockPlayer {
139 pub fn new(scale: PlayerScale, policy: ScaleLockPolicy) -> Self {
141 Self { scale, policy }
142 }
143
144 pub fn from_scale(scale: Scale, policy: ScaleLockPolicy) -> Self {
146 Self::new(PlayerScale::from_scale(scale), policy)
147 }
148
149 pub fn process_pitch(&self, pitch: Pitch) -> Option<Pitch> {
152 match self.policy {
153 ScaleLockPolicy::Quantize => Some(self.scale.nearest_pitch(pitch)),
154 ScaleLockPolicy::Filter => self.scale.contains(pitch.class).then_some(pitch),
155 ScaleLockPolicy::Remap => Some(self.scale.remap_pitch(pitch)),
156 }
157 }
158
159 pub fn process_pitches(&self, pitches: impl IntoIterator<Item = Pitch>) -> Vec<Pitch> {
162 pitches
163 .into_iter()
164 .filter_map(|pitch| self.process_pitch(pitch))
165 .collect()
166 }
167}