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}
19
20#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
22pub enum Mode {
23 Major,
25 MinorNatural,
27 MinorHarmonic,
29 MinorMelodic,
31 Dorian,
33 Phrygian,
35 Lydian,
37 Mixolydian,
39 Aeolian,
41 Locrian,
43 WholeTone,
45 Diminished,
47 Chromatic,
49}
50
51impl Mode {
52 pub fn intervals(self) -> &'static [u8] {
54 match self {
55 Self::Major => &[0, 2, 4, 5, 7, 9, 11],
56 Self::MinorNatural | Self::Aeolian => &[0, 2, 3, 5, 7, 8, 10],
57 Self::MinorHarmonic => &[0, 2, 3, 5, 7, 8, 11],
58 Self::MinorMelodic => &[0, 2, 3, 5, 7, 9, 11],
59 Self::Dorian => &[0, 2, 3, 5, 7, 9, 10],
60 Self::Phrygian => &[0, 1, 3, 5, 7, 8, 10],
61 Self::Lydian => &[0, 2, 4, 6, 7, 9, 11],
62 Self::Mixolydian => &[0, 2, 4, 5, 7, 9, 10],
63 Self::Locrian => &[0, 1, 3, 5, 6, 8, 10],
64 Self::WholeTone => &[0, 2, 4, 6, 8, 10],
65 Self::Diminished => &[0, 2, 3, 5, 6, 8, 9, 11],
66 Self::Chromatic => &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
67 }
68 }
69
70 pub fn name(self) -> &'static str {
72 match self {
73 Self::Major => "major",
74 Self::MinorNatural => "minor-natural",
75 Self::MinorHarmonic => "minor-harmonic",
76 Self::MinorMelodic => "minor-melodic",
77 Self::Dorian => "dorian",
78 Self::Phrygian => "phrygian",
79 Self::Lydian => "lydian",
80 Self::Mixolydian => "mixolydian",
81 Self::Aeolian => "aeolian",
82 Self::Locrian => "locrian",
83 Self::WholeTone => "whole-tone",
84 Self::Diminished => "diminished",
85 Self::Chromatic => "chromatic",
86 }
87 }
88}
89
90#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
92pub struct Key {
93 pub tonic: PitchClass,
95 pub mode: Mode,
97}
98
99#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
111pub struct Scale {
112 pub tonic: PitchClass,
114 pub mode: Mode,
116}
117
118impl Scale {
119 pub const fn new(tonic: PitchClass, mode: Mode) -> Self {
121 Self { tonic, mode }
122 }
123
124 pub const fn major(tonic: PitchClass) -> Self {
126 Self::new(tonic, Mode::Major)
127 }
128 pub const fn minor_natural(tonic: PitchClass) -> Self {
130 Self::new(tonic, Mode::MinorNatural)
131 }
132 pub const fn minor_harmonic(tonic: PitchClass) -> Self {
134 Self::new(tonic, Mode::MinorHarmonic)
135 }
136 pub const fn minor_melodic(tonic: PitchClass) -> Self {
138 Self::new(tonic, Mode::MinorMelodic)
139 }
140 pub const fn dorian(tonic: PitchClass) -> Self {
142 Self::new(tonic, Mode::Dorian)
143 }
144 pub const fn phrygian(tonic: PitchClass) -> Self {
146 Self::new(tonic, Mode::Phrygian)
147 }
148 pub const fn lydian(tonic: PitchClass) -> Self {
150 Self::new(tonic, Mode::Lydian)
151 }
152 pub const fn mixolydian(tonic: PitchClass) -> Self {
154 Self::new(tonic, Mode::Mixolydian)
155 }
156 pub const fn aeolian(tonic: PitchClass) -> Self {
158 Self::new(tonic, Mode::Aeolian)
159 }
160 pub const fn locrian(tonic: PitchClass) -> Self {
162 Self::new(tonic, Mode::Locrian)
163 }
164 pub const fn whole_tone(tonic: PitchClass) -> Self {
166 Self::new(tonic, Mode::WholeTone)
167 }
168 pub const fn diminished(tonic: PitchClass) -> Self {
170 Self::new(tonic, Mode::Diminished)
171 }
172 pub const fn chromatic(tonic: PitchClass) -> Self {
174 Self::new(tonic, Mode::Chromatic)
175 }
176
177 pub fn pitch_classes(self) -> Vec<PitchClass> {
179 self.mode
180 .intervals()
181 .iter()
182 .map(|step| self.tonic.transpose(i32::from(*step)))
183 .collect()
184 }
185
186 pub fn mask(self) -> PitchClassMask {
188 PitchClassMask::from_pitch_classes(&self.pitch_classes())
189 }
190
191 pub fn degree_of(self, pitch_class: PitchClass) -> Option<usize> {
194 self.pitch_classes()
195 .iter()
196 .position(|candidate| *candidate == pitch_class)
197 .map(|index| index + 1)
198 }
199
200 pub fn pitch_at_degree(self, degree: usize) -> PitchClass {
202 let intervals = self.mode.intervals();
203 let index = (degree - 1) % intervals.len();
204 self.tonic.transpose(i32::from(intervals[index]))
205 }
206
207 pub fn transpose_diatonic(self, pitch: Pitch, steps: i32) -> Result<Pitch, PitchScaleError> {
213 let degree = self
214 .degree_of(pitch.class)
215 .ok_or(PitchScaleError::PitchClassOutOfScale(pitch.class.0))?;
216 let intervals = self.mode.intervals();
217 let start = degree as i32 - 1;
218 let target = start + steps;
219 let width = intervals.len() as i32;
220 let octave_delta = target.div_euclid(width);
221 let target_index = target.rem_euclid(width) as usize;
222 let start_semitones = i32::from(intervals[start as usize]);
223 let end_semitones = i32::from(intervals[target_index]) + octave_delta * 12;
224 Ok(pitch.transpose(end_semitones - start_semitones))
225 }
226
227 pub fn chord_tone_to_scale_tone(chord_tone: usize) -> usize {
230 1 + chord_tone.saturating_sub(1) * 2
231 }
232
233 pub fn scale_tone_to_diatonic(scale_tone: usize) -> i32 {
236 scale_tone.saturating_sub(1) as i32
237 }
238}