Skip to main content

sound_to_patch/music/
key.rs

1//! Keys: the key parser, relative and parallel keys, Camelot and Open Key.
2//!
3//! The parser tries three forms in order: a Camelot code (`8A`), an Open
4//! Key code (`1m`), then a letter name with an optional sharp or flat and
5//! mode word (`A`, `Am`, `A minor`, `Bb`, `E♭ moll`, `C sharp minor`). A
6//! bare capital reads as major and a lone lowercase letter as minor; both
7//! readings come back flagged, so the caller records or asks.
8//!
9//! ```
10//! use sound_to_patch::music::key::{Key, Mode, parse_key};
11//!
12//! let reading = parse_key("C sharp minor")?;
13//! assert_eq!(reading.key, Key::new(1, Mode::Minor));
14//! assert_eq!(reading.key.camelot().to_string(), "12A");
15//! assert_eq!(reading.key.open_key().to_string(), "5m");
16//! # Ok::<(), sound_to_patch::music::MusicError>(())
17//! ```
18
19use std::fmt;
20
21use crate::music::MusicError;
22use crate::music::pitch::{Spelling, class_name, letter_class, read_class};
23
24/// Major or minor.
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub enum Mode {
27    /// Major.
28    Major,
29    /// Minor.
30    Minor,
31}
32
33/// A tonic pitch class and a mode.
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
35pub struct Key {
36    /// The tonic, 0 (C) to 11 (B).
37    pub tonic: u8,
38    /// Major or minor.
39    pub mode: Mode,
40}
41
42impl Key {
43    /// A key; the tonic is taken modulo 12.
44    #[must_use]
45    pub fn new(tonic: u8, mode: Mode) -> Self {
46        Self {
47            tonic: tonic % 12,
48            mode,
49        }
50    }
51
52    /// The relative key: the minor a major sixth up, the major a minor
53    /// third up.
54    #[must_use]
55    pub fn relative(self) -> Self {
56        match self.mode {
57            Mode::Major => Self::new(self.tonic + 9, Mode::Minor),
58            Mode::Minor => Self::new(self.tonic + 3, Mode::Major),
59        }
60    }
61
62    /// The parallel key: the same tonic in the other mode.
63    #[must_use]
64    pub fn parallel(self) -> Self {
65        match self.mode {
66            Mode::Major => Self::new(self.tonic, Mode::Minor),
67            Mode::Minor => Self::new(self.tonic, Mode::Major),
68        }
69    }
70
71    /// Sharps or flats for display: flats for F, Bb, Eb, Ab and Db major
72    /// and for D, G, C, F, Bb and Eb minor; sharps otherwise.
73    #[must_use]
74    pub fn spelling(self) -> Spelling {
75        let flats: &[u8] = match self.mode {
76            Mode::Major => &[5, 10, 3, 8, 1],
77            Mode::Minor => &[2, 7, 0, 5, 10, 3],
78        };
79        if flats.contains(&self.tonic) {
80            Spelling::Flats
81        } else {
82            Spelling::Sharps
83        }
84    }
85
86    /// The Camelot code: the number of the major tonic `t` is the `n`
87    /// with `7 * (n - 8) = t` modulo 12; minor keys take the number of
88    /// their relative major.
89    #[must_use]
90    pub fn camelot(self) -> Camelot {
91        let major = match self.mode {
92            Mode::Major => self.tonic,
93            Mode::Minor => self.relative().tonic,
94        };
95        Camelot {
96            number: (7 * major + 7) % 12 + 1,
97            mode: self.mode,
98        }
99    }
100
101    /// The Open Key code: number `(n + 4) mod 12 + 1` of the Camelot number
102    /// `n`.
103    #[must_use]
104    pub fn open_key(self) -> OpenKey {
105        let camelot = self.camelot();
106        OpenKey {
107            number: (camelot.number + 4) % 12 + 1,
108            mode: self.mode,
109        }
110    }
111}
112
113impl fmt::Display for Key {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        let mode = match self.mode {
116            Mode::Major => "major",
117            Mode::Minor => "minor",
118        };
119        write!(f, "{} {mode}", class_name(self.tonic, self.spelling()))
120    }
121}
122
123/// A Camelot code: a number from 1 to 12, `A` for minor and `B` for major.
124#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
125pub struct Camelot {
126    /// 1 to 12.
127    pub number: u8,
128    /// `A` is minor, `B` major.
129    pub mode: Mode,
130}
131
132impl Camelot {
133    /// The key of the code.
134    #[must_use]
135    pub fn key(self) -> Key {
136        // 7 is its own inverse modulo 12, so t = 7 * (n - 8).
137        let major = Key::new((7 * (self.number + 4)) % 12, Mode::Major);
138        match self.mode {
139            Mode::Major => major,
140            Mode::Minor => major.relative(),
141        }
142    }
143
144    /// The code a fifth away: one number up, 12 wrapping to 1.
145    #[must_use]
146    pub fn fifth_up(self) -> Self {
147        Self {
148            number: self.number % 12 + 1,
149            ..self
150        }
151    }
152
153    /// The code a fifth down: one number down, 1 wrapping to 12.
154    #[must_use]
155    pub fn fifth_down(self) -> Self {
156        Self {
157            number: (self.number + 10) % 12 + 1,
158            ..self
159        }
160    }
161}
162
163impl fmt::Display for Camelot {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        let letter = match self.mode {
166            Mode::Minor => 'A',
167            Mode::Major => 'B',
168        };
169        write!(f, "{}{letter}", self.number)
170    }
171}
172
173/// An Open Key code: a number from 1 to 12, `m` for minor and `d` for
174/// major.
175#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
176pub struct OpenKey {
177    /// 1 to 12.
178    pub number: u8,
179    /// `m` is minor, `d` major.
180    pub mode: Mode,
181}
182
183impl OpenKey {
184    /// The key of the code.
185    #[must_use]
186    pub fn key(self) -> Key {
187        Camelot {
188            number: (self.number + 6) % 12 + 1,
189            mode: self.mode,
190        }
191        .key()
192    }
193}
194
195impl fmt::Display for OpenKey {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        let letter = match self.mode {
198            Mode::Minor => 'm',
199            Mode::Major => 'd',
200        };
201        write!(f, "{}{letter}", self.number)
202    }
203}
204
205/// A mode the parser assumed because the text named none.
206#[derive(Clone, Copy, Debug, PartialEq, Eq)]
207pub enum KeyAssumption {
208    /// A bare capital, read as major.
209    BareCapital,
210    /// A lone lowercase letter, read as minor.
211    BareLowercase,
212}
213
214/// A parsed key and the assumption it rests on, if any.
215#[derive(Clone, Copy, Debug, PartialEq, Eq)]
216pub struct KeyReading {
217    /// The key.
218    pub key: Key,
219    /// Set when the mode was not written.
220    pub assumption: Option<KeyAssumption>,
221}
222
223/// Parses a key name.
224///
225/// A key is major or minor. A mode name such as `A dorian` is a scale, so
226/// read it through the scale entries.
227///
228/// # Errors
229///
230/// [`MusicError::UnknownKey`] for text no branch of the grammar accepts,
231/// such as `13A`, `Z` or the chord `Am7`.
232pub fn parse_key(text: &str) -> Result<KeyReading, MusicError> {
233    let unknown = || MusicError::UnknownKey(text.to_owned());
234    let trimmed = text.trim();
235    if let Some(key) = code(trimmed) {
236        return Ok(KeyReading {
237            key,
238            assumption: None,
239        });
240    }
241    letter_key(trimmed).ok_or_else(unknown)
242}
243
244/// A Camelot or Open Key code.
245fn code(text: &str) -> Option<Key> {
246    let split = text.find(|c: char| !c.is_ascii_digit())?;
247    let (digits, rest) = text.split_at(split);
248    if digits.starts_with('0') {
249        return None;
250    }
251    let number: u8 = digits.parse().ok().filter(|n| (1..=12).contains(n))?;
252    match rest.trim() {
253        "A" | "a" => Some(
254            Camelot {
255                number,
256                mode: Mode::Minor,
257            }
258            .key(),
259        ),
260        "B" | "b" => Some(
261            Camelot {
262                number,
263                mode: Mode::Major,
264            }
265            .key(),
266        ),
267        "m" => Some(
268            OpenKey {
269                number,
270                mode: Mode::Minor,
271            }
272            .key(),
273        ),
274        "d" => Some(
275            OpenKey {
276                number,
277                mode: Mode::Major,
278            }
279            .key(),
280        ),
281        _ => None,
282    }
283}
284
285/// A letter name with optional accidentals and a mode word.
286fn letter_key(text: &str) -> Option<KeyReading> {
287    let first = text.chars().next()?;
288    let german = {
289        let lower = text.to_lowercase();
290        lower.ends_with("dur") || lower.ends_with("moll")
291    };
292    let (class, rest) = german_class(text, german).or_else(|| english_class(text))?;
293    let rest = rest.trim();
294    let rest = match rest.strip_prefix('-') {
295        Some(word) if !word.is_empty() => word,
296        _ => rest,
297    };
298    let mode = match rest {
299        "" => None,
300        "M" => Some(Mode::Major),
301        "m" | "-" | "−" | "–" => Some(Mode::Minor),
302        word => match word.to_lowercase().as_str() {
303            "maj" | "major" | "dur" => Some(Mode::Major),
304            "min" | "minor" | "moll" => Some(Mode::Minor),
305            _ => return None,
306        },
307    };
308    let tonic = u8::try_from(class.rem_euclid(12)).ok()?;
309    let (mode, assumption) = match mode {
310        Some(mode) => (mode, None),
311        None if first.is_ascii_lowercase() => (Mode::Minor, Some(KeyAssumption::BareLowercase)),
312        None => (Mode::Major, Some(KeyAssumption::BareCapital)),
313    };
314    Some(KeyReading {
315        key: Key::new(tonic, mode),
316        assumption,
317    })
318}
319
320/// A letter with sign characters or the words `sharp` and `flat`.
321fn english_class(text: &str) -> Option<(i16, &str)> {
322    let (class, rest) = read_class(text)?;
323    let trimmed = rest.trim_start();
324    for (word, step) in [("sharp", 1), ("flat", -1)] {
325        if let Some(after) = trimmed.strip_prefix(word)
326            && ends_word(after)
327        {
328            return Some((class + step, after));
329        }
330    }
331    Some((class, rest))
332}
333
334/// Whether a word ends where `rest` starts.
335fn ends_word(rest: &str) -> bool {
336    rest.is_empty() || rest.starts_with(|c: char| c.is_whitespace() || c == '-')
337}
338
339/// German names, read only when the text uses them: `H` is B, `B` is B
340/// flat before `dur` or `moll`, and `-is` and `-es` add a sharp or a
341/// flat (`Fis`, `Es`, `As`).
342fn german_class(text: &str, german: bool) -> Option<(i16, &str)> {
343    let mut chars = text.chars();
344    let letter = chars.next()?.to_ascii_uppercase();
345    let rest = chars.as_str();
346    let class = match letter {
347        'H' => 11,
348        'B' if german => 10,
349        _ => letter_class(letter)?,
350    };
351    let lower = rest.to_ascii_lowercase();
352    let suffix = [("is", 1), ("es", -1), ("s", -1)]
353        .into_iter()
354        .find(|(suffix, _)| lower.starts_with(suffix) && ends_word(&rest[suffix.len()..]));
355    match suffix {
356        Some(("s", _)) if !matches!(letter, 'A' | 'E') => None,
357        Some((_, _)) if letter == 'B' => None,
358        Some((suffix, step)) => Some((class + step, &rest[suffix.len()..])),
359        None if letter == 'H' || (letter == 'B' && german && ends_word(rest)) => {
360            Some((class, rest))
361        }
362        None => None,
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    fn read(text: &str) -> (Key, Option<KeyAssumption>) {
371        let reading = parse_key(text).unwrap_or_else(|error| panic!("{text}: {error}"));
372        (reading.key, reading.assumption)
373    }
374
375    #[test]
376    fn parses_the_key_forms() {
377        let minor = |tonic| Key::new(tonic, Mode::Minor);
378        let major = |tonic| Key::new(tonic, Mode::Major);
379        let cases = [
380            ("Am", minor(9)),
381            ("A min", minor(9)),
382            ("A minor", minor(9)),
383            ("A-", minor(9)),
384            ("A−", minor(9)),
385            ("A maj", major(9)),
386            ("A major", major(9)),
387            ("AM", major(9)),
388            ("A dur", major(9)),
389            ("A# major", major(10)),
390            ("A♯ maj", major(10)),
391            ("Bb major", major(10)),
392            ("B♭ M", major(10)),
393            ("A sharp major", major(10)),
394            ("B flat minor", minor(10)),
395            ("C sharp minor", minor(1)),
396            ("E♭ moll", minor(3)),
397            ("F## major", major(7)),
398            ("Gbb minor", minor(5)),
399            ("H dur", major(11)),
400            ("B moll", minor(10)),
401            ("Fis moll", minor(6)),
402            ("Es dur", major(3)),
403            ("As dur", major(8)),
404            ("Cis-moll", minor(1)),
405            ("8A", minor(9)),
406            ("8B", major(0)),
407            ("1m", minor(9)),
408            ("1d", major(0)),
409            ("12 B", major(4)),
410        ];
411        for (text, key) in cases {
412            assert_eq!(read(text), (key, None), "{text}");
413        }
414    }
415
416    #[test]
417    fn a_bare_letter_is_flagged() {
418        assert_eq!(
419            read("a"),
420            (Key::new(9, Mode::Minor), Some(KeyAssumption::BareLowercase))
421        );
422        assert_eq!(
423            read("A"),
424            (Key::new(9, Mode::Major), Some(KeyAssumption::BareCapital))
425        );
426        assert_eq!(
427            read("f#"),
428            (Key::new(6, Mode::Minor), Some(KeyAssumption::BareLowercase))
429        );
430        for text in ["A#", "A♯", "Bb", "B♭", "A sharp", "B flat", "H"] {
431            assert_eq!(read(text).1, Some(KeyAssumption::BareCapital), "{text}");
432        }
433    }
434
435    #[test]
436    fn rejects_text_outside_the_grammar() {
437        for text in [
438            "13A", "0A", "Z", "Am7", "A7", "", "8C", "13m", "A minorr", "Bes", "A dorian",
439        ] {
440            assert!(parse_key(text).is_err(), "{text}");
441        }
442    }
443
444    #[test]
445    fn camelot_and_open_key_follow_the_formulas() {
446        for number in 1..=12u8 {
447            let major = Camelot {
448                number,
449                mode: Mode::Major,
450            }
451            .key();
452            assert_eq!(u16::from(major.tonic), (7 * (u16::from(number) + 4)) % 12);
453            let minor = Camelot {
454                number,
455                mode: Mode::Minor,
456            }
457            .key();
458            assert_eq!(minor, major.relative());
459            for key in [major, minor] {
460                assert_eq!(key.camelot().number, number);
461                assert_eq!(key.open_key().number, (number + 4) % 12 + 1);
462                assert_eq!(key.open_key().key(), key);
463            }
464        }
465    }
466
467    #[test]
468    fn every_key_round_trips_through_its_codes() {
469        for tonic in 0..12 {
470            for mode in [Mode::Major, Mode::Minor] {
471                let key = Key::new(tonic, mode);
472                assert_eq!(read(&key.camelot().to_string()).0, key);
473                assert_eq!(read(&key.open_key().to_string()).0, key);
474                assert_eq!(read(&key.to_string()).0, key);
475            }
476        }
477    }
478
479    #[test]
480    fn neighbours_are_a_fifth_or_the_relative_key() {
481        for tonic in 0..12 {
482            for mode in [Mode::Major, Mode::Minor] {
483                let key = Key::new(tonic, mode);
484                let code = key.camelot();
485                assert_eq!(code.fifth_up().key(), Key::new(tonic + 7, mode));
486                assert_eq!(code.fifth_down().key(), Key::new(tonic + 5, mode));
487                assert_eq!(code.fifth_up().fifth_down(), code);
488                let other = Camelot {
489                    mode: key.relative().mode,
490                    ..code
491                };
492                assert_eq!(other.key(), key.relative());
493                assert_eq!(key.parallel().tonic, tonic);
494            }
495        }
496        assert_eq!(
497            Camelot {
498                number: 12,
499                mode: Mode::Major
500            }
501            .fifth_up()
502            .number,
503            1
504        );
505        assert_eq!(
506            Camelot {
507                number: 1,
508                mode: Mode::Major
509            }
510            .fifth_down()
511            .number,
512            12
513        );
514    }
515
516    #[test]
517    fn spells_by_the_display_rule() {
518        assert_eq!(Key::new(10, Mode::Major).to_string(), "Bb major");
519        assert_eq!(Key::new(6, Mode::Minor).to_string(), "F# minor");
520        assert_eq!(Key::new(10, Mode::Minor).to_string(), "Bb minor");
521        assert_eq!(Key::new(8, Mode::Minor).to_string(), "G# minor");
522        assert_eq!(Key::new(3, Mode::Minor).to_string(), "Eb minor");
523        assert_eq!(Key::new(1, Mode::Minor).to_string(), "C# minor");
524    }
525}