1use thiserror::Error;
2
3use sim_lib_pitch_core::{Pitch, PitchClass};
4use sim_lib_pitch_set::PitchClassMask;
5
6#[derive(Debug, Error, Clone, PartialEq, Eq)]
8pub enum PitchScaleError {
9 #[error("pitch class {0} is not in the scale")]
11 PitchClassOutOfScale(u8),
12 #[error("scale must contain at least one pitch class")]
14 EmptyScale,
15 #[error("scale interval {0} is outside 0..12")]
17 InvalidScaleInterval(u8),
18 #[error("invalid scale degree {0}")]
20 InvalidScaleDegree(usize),
21}
22
23#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
25pub enum Mode {
26 Major,
28 MinorNatural,
30 MinorHarmonic,
32 MinorMelodic,
34 Dorian,
36 Phrygian,
38 Lydian,
40 Mixolydian,
42 Aeolian,
44 Locrian,
46 WholeTone,
48 Diminished,
50 Chromatic,
52}
53
54impl Mode {
55 pub fn intervals(self) -> &'static [u8] {
57 match self {
58 Self::Major => &[0, 2, 4, 5, 7, 9, 11],
59 Self::MinorNatural | Self::Aeolian => &[0, 2, 3, 5, 7, 8, 10],
60 Self::MinorHarmonic => &[0, 2, 3, 5, 7, 8, 11],
61 Self::MinorMelodic => &[0, 2, 3, 5, 7, 9, 11],
62 Self::Dorian => &[0, 2, 3, 5, 7, 9, 10],
63 Self::Phrygian => &[0, 1, 3, 5, 7, 8, 10],
64 Self::Lydian => &[0, 2, 4, 6, 7, 9, 11],
65 Self::Mixolydian => &[0, 2, 4, 5, 7, 9, 10],
66 Self::Locrian => &[0, 1, 3, 5, 6, 8, 10],
67 Self::WholeTone => &[0, 2, 4, 6, 8, 10],
68 Self::Diminished => &[0, 2, 3, 5, 6, 8, 9, 11],
69 Self::Chromatic => &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
70 }
71 }
72
73 pub fn name(self) -> &'static str {
75 match self {
76 Self::Major => "major",
77 Self::MinorNatural => "minor-natural",
78 Self::MinorHarmonic => "minor-harmonic",
79 Self::MinorMelodic => "minor-melodic",
80 Self::Dorian => "dorian",
81 Self::Phrygian => "phrygian",
82 Self::Lydian => "lydian",
83 Self::Mixolydian => "mixolydian",
84 Self::Aeolian => "aeolian",
85 Self::Locrian => "locrian",
86 Self::WholeTone => "whole-tone",
87 Self::Diminished => "diminished",
88 Self::Chromatic => "chromatic",
89 }
90 }
91}
92
93#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
95pub struct Key {
96 pub tonic: PitchClass,
98 pub mode: Mode,
100}
101
102#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
114pub struct Scale {
115 pub tonic: PitchClass,
117 pub mode: Mode,
119}
120
121impl Scale {
122 pub const fn new(tonic: PitchClass, mode: Mode) -> Self {
124 Self { tonic, mode }
125 }
126
127 pub const fn major(tonic: PitchClass) -> Self {
129 Self::new(tonic, Mode::Major)
130 }
131 pub const fn minor_natural(tonic: PitchClass) -> Self {
133 Self::new(tonic, Mode::MinorNatural)
134 }
135 pub const fn minor_harmonic(tonic: PitchClass) -> Self {
137 Self::new(tonic, Mode::MinorHarmonic)
138 }
139 pub const fn minor_melodic(tonic: PitchClass) -> Self {
141 Self::new(tonic, Mode::MinorMelodic)
142 }
143 pub const fn dorian(tonic: PitchClass) -> Self {
145 Self::new(tonic, Mode::Dorian)
146 }
147 pub const fn phrygian(tonic: PitchClass) -> Self {
149 Self::new(tonic, Mode::Phrygian)
150 }
151 pub const fn lydian(tonic: PitchClass) -> Self {
153 Self::new(tonic, Mode::Lydian)
154 }
155 pub const fn mixolydian(tonic: PitchClass) -> Self {
157 Self::new(tonic, Mode::Mixolydian)
158 }
159 pub const fn aeolian(tonic: PitchClass) -> Self {
161 Self::new(tonic, Mode::Aeolian)
162 }
163 pub const fn locrian(tonic: PitchClass) -> Self {
165 Self::new(tonic, Mode::Locrian)
166 }
167 pub const fn whole_tone(tonic: PitchClass) -> Self {
169 Self::new(tonic, Mode::WholeTone)
170 }
171 pub const fn diminished(tonic: PitchClass) -> Self {
173 Self::new(tonic, Mode::Diminished)
174 }
175 pub const fn chromatic(tonic: PitchClass) -> Self {
177 Self::new(tonic, Mode::Chromatic)
178 }
179
180 pub fn pitch_classes(self) -> Vec<PitchClass> {
182 self.mode
183 .intervals()
184 .iter()
185 .map(|step| self.tonic.transpose(i32::from(*step)))
186 .collect()
187 }
188
189 pub fn mask(self) -> PitchClassMask {
191 PitchClassMask::from_pitch_classes(&self.pitch_classes())
192 }
193
194 pub fn degree_of(self, pitch_class: PitchClass) -> Option<usize> {
197 self.pitch_classes()
198 .iter()
199 .position(|candidate| *candidate == pitch_class)
200 .map(|index| index + 1)
201 }
202
203 pub fn pitch_at_degree(self, degree: usize) -> Result<PitchClass, PitchScaleError> {
205 self.try_pitch_at_degree(degree)
206 }
207
208 pub fn try_pitch_at_degree(self, degree: usize) -> Result<PitchClass, PitchScaleError> {
210 let index = degree
211 .checked_sub(1)
212 .ok_or(PitchScaleError::InvalidScaleDegree(degree))?;
213 let intervals = self.mode.intervals();
214 Ok(self
215 .tonic
216 .transpose(i32::from(intervals[index % intervals.len()])))
217 }
218
219 pub fn transpose_diatonic(self, pitch: Pitch, steps: i32) -> Result<Pitch, PitchScaleError> {
225 let degree = self
226 .degree_of(pitch.class)
227 .ok_or(PitchScaleError::PitchClassOutOfScale(pitch.class.value()))?;
228 let intervals = self.mode.intervals();
229 let start = degree as i32 - 1;
230 let target = start + steps;
231 let width = intervals.len() as i32;
232 let octave_delta = target.div_euclid(width);
233 let target_index = target.rem_euclid(width) as usize;
234 let start_semitones = i32::from(intervals[start as usize]);
235 let end_semitones = i32::from(intervals[target_index]) + octave_delta * 12;
236 Ok(pitch.transpose(end_semitones - start_semitones))
237 }
238
239 pub fn chord_tone_to_scale_tone(chord_tone: usize) -> Result<usize, PitchScaleError> {
242 let offset = chord_tone
243 .checked_sub(1)
244 .ok_or(PitchScaleError::InvalidScaleDegree(chord_tone))?;
245 offset
246 .checked_mul(2)
247 .and_then(|value| value.checked_add(1))
248 .ok_or(PitchScaleError::InvalidScaleDegree(chord_tone))
249 }
250
251 pub fn scale_tone_to_diatonic(scale_tone: usize) -> Result<i32, PitchScaleError> {
254 let offset = scale_tone
255 .checked_sub(1)
256 .ok_or(PitchScaleError::InvalidScaleDegree(scale_tone))?;
257 i32::try_from(offset).map_err(|_| PitchScaleError::InvalidScaleDegree(scale_tone))
258 }
259}